@hua-labs/tap 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs ADDED
@@ -0,0 +1,4166 @@
1
+ // src/commands/init.ts
2
+ import * as fs5 from "fs";
3
+ import * as path5 from "path";
4
+
5
+ // src/state.ts
6
+ import * as fs2 from "fs";
7
+ import * as path2 from "path";
8
+ import * as crypto from "crypto";
9
+
10
+ // src/config/resolve.ts
11
+ import * as fs from "fs";
12
+ 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";
17
+ function findRepoRoot(startDir = process.cwd()) {
18
+ let dir = path.resolve(startDir);
19
+ while (true) {
20
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
21
+ if (fs.existsSync(path.join(dir, "package.json"))) return dir;
22
+ const parent = path.dirname(dir);
23
+ if (parent === dir) break;
24
+ dir = parent;
25
+ }
26
+ return process.cwd();
27
+ }
28
+ function loadJsonFile(filePath) {
29
+ if (!fs.existsSync(filePath)) return null;
30
+ try {
31
+ const raw = fs.readFileSync(filePath, "utf-8");
32
+ return JSON.parse(raw);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function loadSharedConfig(repoRoot) {
38
+ return loadJsonFile(path.join(repoRoot, SHARED_CONFIG_FILE));
39
+ }
40
+ function loadLocalConfig(repoRoot) {
41
+ return loadJsonFile(path.join(repoRoot, LOCAL_CONFIG_FILE));
42
+ }
43
+ function resolveConfig(overrides = {}, startDir) {
44
+ const repoRoot = findRepoRoot(startDir);
45
+ const shared = loadSharedConfig(repoRoot) ?? {};
46
+ const local = loadLocalConfig(repoRoot) ?? {};
47
+ const sources = {
48
+ repoRoot: "auto",
49
+ commsDir: "auto",
50
+ stateDir: "auto",
51
+ runtimeCommand: "auto",
52
+ appServerUrl: "auto"
53
+ };
54
+ let commsDir;
55
+ if (overrides.commsDir) {
56
+ commsDir = path.resolve(overrides.commsDir);
57
+ sources.commsDir = "cli-flag";
58
+ } else if (process.env.TAP_COMMS_DIR) {
59
+ commsDir = path.resolve(process.env.TAP_COMMS_DIR);
60
+ sources.commsDir = "env";
61
+ } else if (local.commsDir) {
62
+ commsDir = resolvePath(repoRoot, local.commsDir);
63
+ sources.commsDir = "local-config";
64
+ } else if (shared.commsDir) {
65
+ commsDir = resolvePath(repoRoot, shared.commsDir);
66
+ sources.commsDir = "shared-config";
67
+ } else {
68
+ commsDir = path.join(path.dirname(repoRoot), "tap-comms");
69
+ }
70
+ let stateDir;
71
+ if (overrides.stateDir) {
72
+ stateDir = path.resolve(overrides.stateDir);
73
+ sources.stateDir = "cli-flag";
74
+ } else if (process.env.TAP_STATE_DIR) {
75
+ stateDir = path.resolve(process.env.TAP_STATE_DIR);
76
+ sources.stateDir = "env";
77
+ } else if (local.stateDir) {
78
+ stateDir = resolvePath(repoRoot, local.stateDir);
79
+ sources.stateDir = "local-config";
80
+ } else if (shared.stateDir) {
81
+ stateDir = resolvePath(repoRoot, shared.stateDir);
82
+ sources.stateDir = "shared-config";
83
+ } else {
84
+ stateDir = path.join(repoRoot, ".tap-comms");
85
+ }
86
+ let runtimeCommand;
87
+ if (overrides.runtimeCommand) {
88
+ runtimeCommand = overrides.runtimeCommand;
89
+ sources.runtimeCommand = "cli-flag";
90
+ } else if (process.env.TAP_RUNTIME_COMMAND) {
91
+ runtimeCommand = process.env.TAP_RUNTIME_COMMAND;
92
+ sources.runtimeCommand = "env";
93
+ } else if (local.runtimeCommand) {
94
+ runtimeCommand = local.runtimeCommand;
95
+ sources.runtimeCommand = "local-config";
96
+ } else if (shared.runtimeCommand) {
97
+ runtimeCommand = shared.runtimeCommand;
98
+ sources.runtimeCommand = "shared-config";
99
+ } else {
100
+ runtimeCommand = DEFAULT_RUNTIME_COMMAND;
101
+ }
102
+ let appServerUrl;
103
+ if (overrides.appServerUrl) {
104
+ appServerUrl = overrides.appServerUrl;
105
+ sources.appServerUrl = "cli-flag";
106
+ } else if (process.env.TAP_APP_SERVER_URL) {
107
+ appServerUrl = process.env.TAP_APP_SERVER_URL;
108
+ sources.appServerUrl = "env";
109
+ } else if (local.appServerUrl) {
110
+ appServerUrl = local.appServerUrl;
111
+ sources.appServerUrl = "local-config";
112
+ } else if (shared.appServerUrl) {
113
+ appServerUrl = shared.appServerUrl;
114
+ sources.appServerUrl = "shared-config";
115
+ } else {
116
+ appServerUrl = DEFAULT_APP_SERVER_URL;
117
+ }
118
+ return {
119
+ config: { repoRoot, commsDir, stateDir, runtimeCommand, appServerUrl },
120
+ sources
121
+ };
122
+ }
123
+ function resolvePath(repoRoot, p) {
124
+ return path.isAbsolute(p) ? p : path.resolve(repoRoot, p);
125
+ }
126
+
127
+ // src/state.ts
128
+ var STATE_FILE = "state.json";
129
+ var SCHEMA_VERSION = 2;
130
+ function getStateDir(repoRoot) {
131
+ const { config } = resolveConfig({}, repoRoot);
132
+ return config.stateDir;
133
+ }
134
+ function getStatePath(repoRoot) {
135
+ return path2.join(getStateDir(repoRoot), STATE_FILE);
136
+ }
137
+ function stateExists(repoRoot) {
138
+ return fs2.existsSync(getStatePath(repoRoot));
139
+ }
140
+ function migrateStateV1toV2(v1) {
141
+ const instances = {};
142
+ for (const [runtime, rs] of Object.entries(v1.runtimes)) {
143
+ if (!rs) continue;
144
+ const instanceId = runtime;
145
+ instances[instanceId] = {
146
+ instanceId,
147
+ runtime,
148
+ agentName: null,
149
+ port: null,
150
+ headless: null,
151
+ ...rs
152
+ };
153
+ }
154
+ return {
155
+ schemaVersion: SCHEMA_VERSION,
156
+ createdAt: v1.createdAt,
157
+ updatedAt: v1.updatedAt,
158
+ commsDir: v1.commsDir,
159
+ repoRoot: v1.repoRoot,
160
+ packageVersion: v1.packageVersion,
161
+ instances
162
+ };
163
+ }
164
+ function loadState(repoRoot) {
165
+ const statePath = getStatePath(repoRoot);
166
+ if (!fs2.existsSync(statePath)) return null;
167
+ const raw = fs2.readFileSync(statePath, "utf-8");
168
+ const parsed = JSON.parse(raw);
169
+ if (parsed.schemaVersion === 1 || parsed.runtimes) {
170
+ const migrated = migrateStateV1toV2(parsed);
171
+ saveState(repoRoot, migrated);
172
+ return migrated;
173
+ }
174
+ return parsed;
175
+ }
176
+ function saveState(repoRoot, state) {
177
+ const stateDir = getStateDir(repoRoot);
178
+ fs2.mkdirSync(stateDir, { recursive: true });
179
+ const statePath = getStatePath(repoRoot);
180
+ const tmp = `${statePath}.tmp.${process.pid}`;
181
+ fs2.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
182
+ fs2.renameSync(tmp, statePath);
183
+ }
184
+ function createInitialState(commsDir, repoRoot, packageVersion) {
185
+ const now = (/* @__PURE__ */ new Date()).toISOString();
186
+ return {
187
+ schemaVersion: SCHEMA_VERSION,
188
+ createdAt: now,
189
+ updatedAt: now,
190
+ commsDir: path2.resolve(commsDir),
191
+ repoRoot: path2.resolve(repoRoot),
192
+ packageVersion,
193
+ instances: {}
194
+ };
195
+ }
196
+ function updateInstanceState(state, instanceId, instanceState) {
197
+ return {
198
+ ...state,
199
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
200
+ instances: {
201
+ ...state.instances,
202
+ [instanceId]: instanceState
203
+ }
204
+ };
205
+ }
206
+ function removeInstanceState(state, instanceId) {
207
+ const { [instanceId]: _removed, ...remaining } = state.instances;
208
+ return {
209
+ ...state,
210
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
211
+ instances: remaining
212
+ };
213
+ }
214
+ function getInstalledInstances(state) {
215
+ return Object.keys(state.instances).filter(
216
+ (id) => state.instances[id]?.installed
217
+ );
218
+ }
219
+ function ensureBackupDir(stateDir, instanceId) {
220
+ const backupDir = path2.join(stateDir, "backups", instanceId);
221
+ fs2.mkdirSync(backupDir, { recursive: true });
222
+ return backupDir;
223
+ }
224
+ function backupFile(filePath, backupDir) {
225
+ const basename3 = path2.basename(filePath);
226
+ const hash = fileHash(filePath);
227
+ const backupPath = path2.join(backupDir, `${basename3}.${hash}.bak`);
228
+ fs2.copyFileSync(filePath, backupPath);
229
+ return backupPath;
230
+ }
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;
352
+ }
353
+
354
+ // src/version.ts
355
+ var version = "0.1.0";
356
+
357
+ // src/permissions.ts
358
+ import * as fs4 from "fs";
359
+ import * as path4 from "path";
360
+ import * as os from "os";
361
+
362
+ // src/toml.ts
363
+ function splitLines(content) {
364
+ return content.replace(/\r\n/g, "\n").split("\n");
365
+ }
366
+ function tableHeader(selector) {
367
+ return `[${selector}]`;
368
+ }
369
+ function findTableRange(lines, selector) {
370
+ const header = tableHeader(selector);
371
+ for (let i = 0; i < lines.length; i++) {
372
+ if (lines[i].trim() !== header) continue;
373
+ let end = lines.length;
374
+ for (let j = i + 1; j < lines.length; j++) {
375
+ const trimmed = lines[j].trim();
376
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
377
+ end = j;
378
+ break;
379
+ }
380
+ }
381
+ return { start: i, end };
382
+ }
383
+ return null;
384
+ }
385
+ function escapeBasicString(value) {
386
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
387
+ }
388
+ function renderValue(value) {
389
+ if (Array.isArray(value)) {
390
+ return `[${value.map((item) => `"${escapeBasicString(item)}"`).join(", ")}]`;
391
+ }
392
+ return `"${escapeBasicString(value)}"`;
393
+ }
394
+ function extractTomlTable(content, selector) {
395
+ const lines = splitLines(content);
396
+ const range = findTableRange(lines, selector);
397
+ if (!range) return null;
398
+ return `${lines.slice(range.start, range.end).join("\n")}
399
+ `;
400
+ }
401
+ function removeTomlTable(content, selector) {
402
+ const lines = splitLines(content);
403
+ const range = findTableRange(lines, selector);
404
+ if (!range) return content;
405
+ const next = [...lines.slice(0, range.start), ...lines.slice(range.end)];
406
+ return `${trimTomlDocument(next.join("\n"))}
407
+ `;
408
+ }
409
+ function replaceTomlTable(content, selector, replacement) {
410
+ const lines = splitLines(content);
411
+ const range = findTableRange(lines, selector);
412
+ const replacementLines = replacement.replace(/\r\n/g, "\n").trimEnd().split("\n");
413
+ if (!range) {
414
+ const doc = trimTomlDocument(content);
415
+ if (!doc) return `${replacement.trimEnd()}
416
+ `;
417
+ return `${doc}
418
+
419
+ ${replacement.trimEnd()}
420
+ `;
421
+ }
422
+ const next = [
423
+ ...lines.slice(0, range.start),
424
+ ...replacementLines,
425
+ ...lines.slice(range.end)
426
+ ];
427
+ return `${trimTomlDocument(next.join("\n"))}
428
+ `;
429
+ }
430
+ function renderTomlTable(selector, entries, existingContent) {
431
+ const preserved = parseTomlAssignments(existingContent ?? "");
432
+ const merged = { ...preserved, ...entries };
433
+ const lines = [tableHeader(selector)];
434
+ for (const [key, value] of Object.entries(merged)) {
435
+ lines.push(`${key} = ${renderValue(value)}`);
436
+ }
437
+ return `${lines.join("\n")}
438
+ `;
439
+ }
440
+ function parseTomlAssignments(tableContent) {
441
+ const lines = splitLines(tableContent);
442
+ const values = {};
443
+ for (const rawLine of lines) {
444
+ const line = rawLine.trim();
445
+ if (!line || line.startsWith("#") || line.startsWith("[") && line.endsWith("]")) {
446
+ continue;
447
+ }
448
+ const match = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
449
+ if (!match) continue;
450
+ const [, key, rawValue] = match;
451
+ const value = rawValue.trim();
452
+ if (value.startsWith("[") && value.endsWith("]")) {
453
+ const items = value.slice(1, -1).split(",").map((item) => item.trim()).filter(Boolean).map(unquoteTomlString);
454
+ values[key] = items;
455
+ continue;
456
+ }
457
+ values[key] = unquoteTomlString(value);
458
+ }
459
+ return values;
460
+ }
461
+ function trimTomlDocument(content) {
462
+ return content.replace(/\s+$/g, "").replace(/\n{3,}/g, "\n\n");
463
+ }
464
+ function unquoteTomlString(value) {
465
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
466
+ const inner = value.slice(1, -1);
467
+ return value.startsWith('"') ? inner.replace(/\\"/g, '"').replace(/\\\\/g, "\\") : inner;
468
+ }
469
+ return value;
470
+ }
471
+
472
+ // src/permissions.ts
473
+ var CLAUDE_DENY_RULES = [
474
+ "Bash(git push --force:*)",
475
+ "Bash(git push -f:*)",
476
+ "Bash(git push --force-with-lease:*)",
477
+ "Bash(git reset --hard:*)",
478
+ "Bash(git checkout -- .:*)",
479
+ "Bash(git clean -f:*)",
480
+ "Bash(git clean -fd:*)",
481
+ "Bash(git clean -fdx:*)",
482
+ "Bash(git restore --source=:*)",
483
+ "Bash(git branch -D:*)",
484
+ "Bash(git stash drop:*)",
485
+ "Bash(rm -rf:*)"
486
+ ];
487
+ function applyClaudePermissions(repoRoot, mode) {
488
+ const warnings = [];
489
+ const claudeDir = path4.join(repoRoot, ".claude");
490
+ const settingsPath = path4.join(claudeDir, "settings.local.json");
491
+ fs4.mkdirSync(claudeDir, { recursive: true });
492
+ let settings = {};
493
+ if (fs4.existsSync(settingsPath)) {
494
+ try {
495
+ settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
496
+ } catch {
497
+ warnings.push(
498
+ ".claude/settings.local.json was invalid JSON. Starting fresh."
499
+ );
500
+ settings = {};
501
+ }
502
+ }
503
+ const existingDeny = Array.isArray(settings.deny) ? settings.deny : [];
504
+ if (mode === "full") {
505
+ const tapRuleSet = new Set(CLAUDE_DENY_RULES);
506
+ const cleaned = existingDeny.filter((r) => !tapRuleSet.has(r));
507
+ settings.deny = cleaned;
508
+ const tmp2 = `${settingsPath}.tmp.${process.pid}`;
509
+ fs4.writeFileSync(tmp2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
510
+ fs4.renameSync(tmp2, settingsPath);
511
+ logWarn("Claude: full mode \u2014 tap deny rules removed. Use with caution.");
512
+ warnings.push("Full permission mode: tap deny rules removed.");
513
+ return { applied: true, warnings };
514
+ }
515
+ const newDeny = [.../* @__PURE__ */ new Set([...existingDeny, ...CLAUDE_DENY_RULES])];
516
+ settings.deny = newDeny;
517
+ const tmp = `${settingsPath}.tmp.${process.pid}`;
518
+ fs4.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
519
+ fs4.renameSync(tmp, settingsPath);
520
+ logSuccess(
521
+ `Claude: ${CLAUDE_DENY_RULES.length} deny rules applied to .claude/settings.local.json`
522
+ );
523
+ return { applied: true, warnings };
524
+ }
525
+ function findCodexConfigPath() {
526
+ return path4.join(os.homedir(), ".codex", "config.toml");
527
+ }
528
+ function canonicalizeTrustPath(targetPath) {
529
+ let resolved = path4.resolve(targetPath).replace(/\//g, "\\");
530
+ const driveRoot = /^[A-Za-z]:\\$/;
531
+ if (!driveRoot.test(resolved)) {
532
+ resolved = resolved.replace(/\\+$/g, "");
533
+ }
534
+ return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
535
+ }
536
+ function applyCodexPermissions(repoRoot, commsDir, mode) {
537
+ const warnings = [];
538
+ const configPath = findCodexConfigPath();
539
+ fs4.mkdirSync(path4.dirname(configPath), { recursive: true });
540
+ let content = "";
541
+ if (fs4.existsSync(configPath)) {
542
+ content = fs4.readFileSync(configPath, "utf-8");
543
+ }
544
+ const trustTargets = getCodexWritableRoots(repoRoot, commsDir);
545
+ if (mode === "full") {
546
+ logWarn("Codex: full mode \u2014 setting sandbox to danger-full-access.");
547
+ warnings.push(
548
+ "Full mode: sandbox set to danger-full-access. Use with caution."
549
+ );
550
+ content = replaceTomlTable(
551
+ content,
552
+ "sandbox",
553
+ renderTomlTable(
554
+ "sandbox",
555
+ { mode: "danger-full-access" },
556
+ extractTomlTable(content, "sandbox")
557
+ )
558
+ );
559
+ } else {
560
+ content = replaceTomlTable(
561
+ content,
562
+ "sandbox",
563
+ renderTomlTable(
564
+ "sandbox",
565
+ { mode: "workspace-write", network_access: "full" },
566
+ extractTomlTable(content, "sandbox")
567
+ )
568
+ );
569
+ const forwardSlashRoots = trustTargets.map((r) => r.replace(/\\/g, "/"));
570
+ content = replaceTomlTable(
571
+ content,
572
+ "sandbox_workspace_write",
573
+ renderTomlTable(
574
+ "sandbox_workspace_write",
575
+ { writable_roots: forwardSlashRoots },
576
+ extractTomlTable(content, "sandbox_workspace_write")
577
+ )
578
+ );
579
+ if (process.platform === "win32") {
580
+ content = replaceTomlTable(
581
+ content,
582
+ "windows",
583
+ renderTomlTable(
584
+ "windows",
585
+ { sandbox: "elevated" },
586
+ extractTomlTable(content, "windows")
587
+ )
588
+ );
589
+ }
590
+ }
591
+ for (const target of trustTargets) {
592
+ const selector = `projects.'${canonicalizeTrustPath(target)}'`;
593
+ content = replaceTomlTable(
594
+ content,
595
+ selector,
596
+ renderTomlTable(
597
+ selector,
598
+ { trust_level: "trusted" },
599
+ extractTomlTable(content, selector)
600
+ )
601
+ );
602
+ }
603
+ const tmp = `${configPath}.tmp.${process.pid}`;
604
+ fs4.writeFileSync(tmp, content, "utf-8");
605
+ fs4.renameSync(tmp, configPath);
606
+ const modeLabel = mode === "full" ? "danger-full-access" : "workspace-write, network=full";
607
+ logSuccess(
608
+ `Codex: sandbox=${modeLabel}, ${trustTargets.length} path(s) trusted`
609
+ );
610
+ return { applied: true, warnings };
611
+ }
612
+ function getCodexWritableRoots(repoRoot, commsDir) {
613
+ const roots = [repoRoot, commsDir];
614
+ const parent = path4.dirname(repoRoot);
615
+ for (let i = 1; i <= 4; i++) {
616
+ const wtPath = path4.join(parent, `hua-wt-${i}`);
617
+ if (fs4.existsSync(wtPath)) roots.push(wtPath);
618
+ }
619
+ return [...new Set(roots.map((r) => path4.resolve(r)))];
620
+ }
621
+ function buildPermissionSummary(mode, repoRoot, commsDir) {
622
+ const trustedPaths = getCodexWritableRoots(repoRoot, commsDir);
623
+ return {
624
+ mode,
625
+ claude: {
626
+ applied: true,
627
+ denyCount: mode === "safe" ? CLAUDE_DENY_RULES.length : 0,
628
+ warnings: mode === "full" ? ["Full mode: tap deny rules removed."] : []
629
+ },
630
+ codex: {
631
+ applied: true,
632
+ trustedPaths,
633
+ warnings: mode === "full" ? ["Full mode: sandbox set to danger-full-access."] : []
634
+ }
635
+ };
636
+ }
637
+
638
+ // src/commands/init.ts
639
+ var COMMS_DIRS = [
640
+ "inbox",
641
+ "reviews",
642
+ "findings",
643
+ "handoff",
644
+ "retros",
645
+ "archive"
646
+ ];
647
+ function parsePermissionMode(args) {
648
+ const idx = args.indexOf("--permissions");
649
+ if (idx !== -1 && args[idx + 1]) {
650
+ const value = args[idx + 1];
651
+ if (value === "full" || value === "safe") return value;
652
+ logWarn(`Unknown permission mode: ${value}. Using "safe".`);
653
+ }
654
+ return "safe";
655
+ }
656
+ async function initCommand(args) {
657
+ const repoRoot = findRepoRoot2();
658
+ const commsDir = resolveCommsDir(args, repoRoot);
659
+ const permMode = parsePermissionMode(args);
660
+ if (stateExists(repoRoot) && !args.includes("--force")) {
661
+ return {
662
+ ok: true,
663
+ command: "init",
664
+ code: "TAP_ALREADY_INITIALIZED",
665
+ message: "Already initialized. Use --force to re-initialize.",
666
+ warnings: [],
667
+ data: { commsDir, repoRoot }
668
+ };
669
+ }
670
+ logHeader("@hua-labs/tap init");
671
+ log(`Comms directory: ${commsDir}`);
672
+ for (const dir of COMMS_DIRS) {
673
+ const dirPath = path5.join(commsDir, dir);
674
+ fs5.mkdirSync(dirPath, { recursive: true });
675
+ logSuccess(`Created ${dir}/`);
676
+ }
677
+ const gitignorePath = path5.join(commsDir, ".gitignore");
678
+ if (!fs5.existsSync(gitignorePath)) {
679
+ fs5.writeFileSync(
680
+ gitignorePath,
681
+ ["tap.db", ".lock", "*.tmp.*", ".DS_Store"].join("\n") + "\n",
682
+ "utf-8"
683
+ );
684
+ logSuccess("Created .gitignore");
685
+ }
686
+ const { config } = resolveConfig({}, repoRoot);
687
+ 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);
692
+ logSuccess(`Created ${stateDirRel}/ state directory`);
693
+ const repoGitignore = path5.join(repoRoot, ".gitignore");
694
+ const gitignoreEntries = [
695
+ { entry: stateDirRel.replace(/\\/g, "/") + "/", label: "tap-comms state" },
696
+ {
697
+ entry: "tap-config.local.json",
698
+ label: "tap-comms local config (machine-specific)"
699
+ }
700
+ ];
701
+ if (fs5.existsSync(repoGitignore)) {
702
+ const content = fs5.readFileSync(repoGitignore, "utf-8");
703
+ for (const { entry, label } of gitignoreEntries) {
704
+ if (!content.includes(entry)) {
705
+ fs5.appendFileSync(repoGitignore, `
706
+ # ${label}
707
+ ${entry}
708
+ `);
709
+ logSuccess(`Added ${entry} to .gitignore`);
710
+ }
711
+ }
712
+ }
713
+ const state = createInitialState(commsDir, repoRoot, version);
714
+ saveState(repoRoot, state);
715
+ logSuccess("Created state.json");
716
+ const warnings = [];
717
+ logHeader(`Permissions: ${permMode} mode`);
718
+ const claudeResult = applyClaudePermissions(repoRoot, permMode);
719
+ warnings.push(...claudeResult.warnings);
720
+ const codexResult = applyCodexPermissions(repoRoot, commsDir, permMode);
721
+ warnings.push(...codexResult.warnings);
722
+ const permSummary = buildPermissionSummary(permMode, repoRoot, commsDir);
723
+ if (permMode === "full") {
724
+ logWarn("Full mode: no destructive operation guards. Use with caution.");
725
+ }
726
+ logHeader("Done! Next steps:");
727
+ log("npx @hua-labs/tap add claude # Add Claude runtime");
728
+ log("npx @hua-labs/tap add codex # Add Codex runtime");
729
+ log("npx @hua-labs/tap status # Check status");
730
+ return {
731
+ ok: true,
732
+ command: "init",
733
+ code: "TAP_INIT_OK",
734
+ message: "Initialized successfully",
735
+ warnings,
736
+ data: {
737
+ commsDir,
738
+ repoRoot,
739
+ permissions: permSummary
740
+ }
741
+ };
742
+ }
743
+
744
+ // 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");
751
+ }
752
+ function findClaudeCommand() {
753
+ try {
754
+ execSync("claude --version", { stdio: "pipe" });
755
+ return "claude";
756
+ } catch {
757
+ return null;
758
+ }
759
+ }
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) {
771
+ const candidates = [
772
+ path6.join(
773
+ ctx.repoRoot,
774
+ "packages",
775
+ "tap-plugin",
776
+ "channels",
777
+ "tap-comms.ts"
778
+ ),
779
+ path6.join(
780
+ ctx.repoRoot,
781
+ "node_modules",
782
+ "@hua-labs",
783
+ "channels",
784
+ "tap-comms.ts"
785
+ )
786
+ ];
787
+ for (const p of candidates) {
788
+ if (fs6.existsSync(p)) return p;
789
+ }
790
+ return null;
791
+ }
792
+ var claudeAdapter = {
793
+ runtime: "claude",
794
+ async probe(ctx) {
795
+ const warnings = [];
796
+ const issues = [];
797
+ const configPath = findMcpJsonPath(ctx);
798
+ const configExists = fs6.existsSync(configPath);
799
+ const runtimeCommand = findClaudeCommand();
800
+ const canWrite = configExists ? (() => {
801
+ try {
802
+ fs6.accessSync(configPath, fs6.constants.W_OK);
803
+ return true;
804
+ } catch {
805
+ return false;
806
+ }
807
+ })() : true;
808
+ if (!runtimeCommand) {
809
+ warnings.push(
810
+ "Claude CLI not found in PATH. Config will be created but may need manual setup."
811
+ );
812
+ }
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)) {
820
+ issues.push(
821
+ `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
822
+ );
823
+ }
824
+ return {
825
+ installed: true,
826
+ // Claude adapter always "installed" — .mcp.json is per-project
827
+ configPath,
828
+ configExists,
829
+ runtimeCommand,
830
+ version: null,
831
+ canWrite,
832
+ warnings,
833
+ issues
834
+ };
835
+ },
836
+ async plan(ctx, probe) {
837
+ const configPath = probe.configPath ?? findMcpJsonPath(ctx);
838
+ const conflicts = [];
839
+ const warnings = [];
840
+ const operations = [];
841
+ const ownedArtifacts = [];
842
+ if (probe.configExists) {
843
+ const raw = fs6.readFileSync(configPath, "utf-8");
844
+ try {
845
+ const config = JSON.parse(raw);
846
+ if (config.mcpServers?.[MCP_SERVER_KEY]) {
847
+ conflicts.push(
848
+ `Existing "${MCP_SERVER_KEY}" entry in .mcp.json will be overwritten.`
849
+ );
850
+ }
851
+ } catch {
852
+ warnings.push(
853
+ ".mcp.json exists but is not valid JSON. Will be overwritten."
854
+ );
855
+ }
856
+ }
857
+ const serverEntry = buildMcpServerEntry(ctx);
858
+ if (!serverEntry) {
859
+ warnings.push(
860
+ "tap-comms MCP server not found locally. Skipping .mcp.json patch. Run from monorepo root with packages/tap-plugin/channels/ available."
861
+ );
862
+ return {
863
+ runtime: "claude",
864
+ operations: [],
865
+ ownedArtifacts: [],
866
+ backupDir: ensureBackupDir(ctx.stateDir, "claude"),
867
+ restartRequired: false,
868
+ conflicts,
869
+ warnings
870
+ };
871
+ }
872
+ operations.push({
873
+ type: probe.configExists ? "merge" : "set",
874
+ path: configPath,
875
+ key: `mcpServers.${MCP_SERVER_KEY}`,
876
+ value: serverEntry
877
+ });
878
+ ownedArtifacts.push({
879
+ kind: "json-path",
880
+ path: configPath,
881
+ selector: `mcpServers.${MCP_SERVER_KEY}`
882
+ });
883
+ const backupDir = ensureBackupDir(ctx.stateDir, "claude");
884
+ return {
885
+ runtime: "claude",
886
+ operations,
887
+ ownedArtifacts,
888
+ backupDir,
889
+ restartRequired: true,
890
+ conflicts,
891
+ warnings
892
+ };
893
+ },
894
+ async apply(_ctx, plan) {
895
+ const changedFiles = [];
896
+ const warnings = [];
897
+ let appliedOps = 0;
898
+ for (const op of plan.operations) {
899
+ try {
900
+ if (op.type === "set" || op.type === "merge") {
901
+ let config = {};
902
+ if (fs6.existsSync(op.path)) {
903
+ backupFile(op.path, plan.backupDir);
904
+ const raw = fs6.readFileSync(op.path, "utf-8");
905
+ try {
906
+ config = JSON.parse(raw);
907
+ } catch {
908
+ warnings.push(
909
+ `${op.path} was invalid JSON. Created backup and starting fresh.`
910
+ );
911
+ }
912
+ }
913
+ if (op.key) {
914
+ setNestedKey(config, op.key, op.value);
915
+ }
916
+ const tmp = `${op.path}.tmp.${process.pid}`;
917
+ fs6.writeFileSync(
918
+ tmp,
919
+ JSON.stringify(config, null, 2) + "\n",
920
+ "utf-8"
921
+ );
922
+ fs6.renameSync(tmp, op.path);
923
+ changedFiles.push(op.path);
924
+ appliedOps++;
925
+ }
926
+ } catch (err) {
927
+ warnings.push(
928
+ `Failed to apply op on ${op.path}: ${err instanceof Error ? err.message : String(err)}`
929
+ );
930
+ }
931
+ }
932
+ const lastAppliedHash = changedFiles.length > 0 ? fileHash(changedFiles[0]) : "";
933
+ return {
934
+ success: appliedOps > 0,
935
+ appliedOps,
936
+ backupCreated: true,
937
+ lastAppliedHash,
938
+ ownedArtifacts: plan.ownedArtifacts,
939
+ changedFiles,
940
+ restartRequired: plan.restartRequired,
941
+ warnings
942
+ };
943
+ },
944
+ async verify(ctx, plan) {
945
+ const checks = [];
946
+ const warnings = [];
947
+ const configPath = plan.operations[0]?.path;
948
+ if (configPath) {
949
+ checks.push({
950
+ name: "Config file exists",
951
+ passed: fs6.existsSync(configPath),
952
+ message: fs6.existsSync(configPath) ? void 0 : `${configPath} not found`
953
+ });
954
+ if (fs6.existsSync(configPath)) {
955
+ try {
956
+ const raw = fs6.readFileSync(configPath, "utf-8");
957
+ const config = JSON.parse(raw);
958
+ checks.push({ name: "Config is valid JSON", passed: true });
959
+ const entry = config.mcpServers?.[MCP_SERVER_KEY];
960
+ checks.push({
961
+ name: "tap-comms entry present",
962
+ passed: !!entry,
963
+ message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
964
+ });
965
+ if (entry) {
966
+ const hasCommsDir = entry.env?.TAP_COMMS_DIR === ctx.commsDir;
967
+ checks.push({
968
+ name: "TAP_COMMS_DIR configured",
969
+ passed: hasCommsDir,
970
+ message: hasCommsDir ? void 0 : `Expected ${ctx.commsDir}`
971
+ });
972
+ }
973
+ } catch {
974
+ checks.push({
975
+ name: "Config is valid JSON",
976
+ passed: false,
977
+ message: "Parse error"
978
+ });
979
+ }
980
+ }
981
+ }
982
+ checks.push({
983
+ name: "Comms directory exists",
984
+ passed: fs6.existsSync(ctx.commsDir),
985
+ message: fs6.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
986
+ });
987
+ const cmd = findClaudeCommand();
988
+ checks.push({
989
+ name: "Claude CLI found",
990
+ passed: !!cmd,
991
+ message: cmd ? void 0 : "claude not in PATH (non-blocking)"
992
+ });
993
+ if (!cmd) {
994
+ warnings.push(
995
+ "Claude CLI not in PATH. Config is ready but cannot verify runtime reads it."
996
+ );
997
+ }
998
+ const ok = checks.filter((c) => c.name !== "Claude CLI found").every((c) => c.passed);
999
+ return { ok, checks, restartRequired: true, warnings };
1000
+ },
1001
+ bridgeMode() {
1002
+ return "native-push";
1003
+ }
1004
+ };
1005
+ function setNestedKey(obj, keyPath, value) {
1006
+ const keys = keyPath.split(".");
1007
+ let current = obj;
1008
+ for (let i = 0; i < keys.length - 1; i++) {
1009
+ const key = keys[i];
1010
+ if (typeof current[key] !== "object" || current[key] === null) {
1011
+ current[key] = {};
1012
+ }
1013
+ current = current[key];
1014
+ }
1015
+ current[keys[keys.length - 1]] = value;
1016
+ }
1017
+
1018
+ // src/adapters/codex.ts
1019
+ import * as fs9 from "fs";
1020
+ import * as path9 from "path";
1021
+ import { fileURLToPath } from "url";
1022
+
1023
+ // src/artifact-backups.ts
1024
+ import * as crypto2 from "crypto";
1025
+ import * as fs7 from "fs";
1026
+ import * as path7 from "path";
1027
+ function selectorHash(selector) {
1028
+ return crypto2.createHash("sha256").update(selector).digest("hex").slice(0, 12);
1029
+ }
1030
+ function artifactBackupPath(backupDir, kind, selector) {
1031
+ const safeKind = kind.replace(/[^a-z-]/gi, "-");
1032
+ return path7.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
1033
+ }
1034
+ function writeArtifactBackup(backupPath, payload) {
1035
+ fs7.mkdirSync(path7.dirname(backupPath), { recursive: true });
1036
+ const tmp = `${backupPath}.tmp.${process.pid}`;
1037
+ fs7.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
1038
+ fs7.renameSync(tmp, backupPath);
1039
+ }
1040
+ function readArtifactBackup(backupPath) {
1041
+ if (!fs7.existsSync(backupPath)) return null;
1042
+ try {
1043
+ const raw = fs7.readFileSync(backupPath, "utf-8");
1044
+ return JSON.parse(raw);
1045
+ } catch {
1046
+ return null;
1047
+ }
1048
+ }
1049
+
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
+ // src/adapters/codex.ts
1166
+ var MCP_SELECTOR = "mcp_servers.tap-comms";
1167
+ var ENV_SELECTOR = "mcp_servers.tap-comms.env";
1168
+ function findCodexConfigPath2() {
1169
+ return path9.join(getHomeDir(), ".codex", "config.toml");
1170
+ }
1171
+ function canonicalizeTrustPath2(targetPath) {
1172
+ let resolved = path9.resolve(targetPath).replace(/\//g, "\\");
1173
+ const driveRoot = /^[A-Za-z]:\\$/;
1174
+ if (!driveRoot.test(resolved)) {
1175
+ resolved = resolved.replace(/\\+$/g, "");
1176
+ }
1177
+ return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
1178
+ }
1179
+ function trustSelector(targetPath) {
1180
+ return `projects.'${canonicalizeTrustPath2(targetPath)}'`;
1181
+ }
1182
+ function getTrustTargets(ctx) {
1183
+ const targets = [ctx.repoRoot, process.cwd()];
1184
+ return [...new Set(targets.map((value) => path9.resolve(value)))];
1185
+ }
1186
+ function buildManagedArtifacts(configPath, ctx) {
1187
+ const artifacts = [
1188
+ { kind: "toml-table", path: configPath, selector: MCP_SELECTOR },
1189
+ { kind: "toml-table", path: configPath, selector: ENV_SELECTOR }
1190
+ ];
1191
+ for (const target of getTrustTargets(ctx)) {
1192
+ artifacts.push({
1193
+ kind: "toml-table",
1194
+ path: configPath,
1195
+ selector: trustSelector(target)
1196
+ });
1197
+ }
1198
+ return artifacts;
1199
+ }
1200
+ function readConfigOrEmpty(configPath) {
1201
+ if (!fs9.existsSync(configPath)) return "";
1202
+ return fs9.readFileSync(configPath, "utf-8");
1203
+ }
1204
+ function writeTomlFile(filePath, content) {
1205
+ fs9.mkdirSync(path9.dirname(filePath), { recursive: true });
1206
+ const tmp = `${filePath}.tmp.${process.pid}`;
1207
+ fs9.writeFileSync(tmp, content, "utf-8");
1208
+ fs9.renameSync(tmp, filePath);
1209
+ }
1210
+ function verifyManagedToml(content, ctx, configPath) {
1211
+ const checks = [];
1212
+ const managed = buildManagedMcpServerSpec(ctx);
1213
+ const mainTable = extractTomlTable(content, MCP_SELECTOR);
1214
+ const envTable = extractTomlTable(content, ENV_SELECTOR);
1215
+ checks.push({
1216
+ name: "Codex config exists",
1217
+ passed: fs9.existsSync(configPath),
1218
+ message: fs9.existsSync(configPath) ? void 0 : `${configPath} not found`
1219
+ });
1220
+ checks.push({
1221
+ name: "tap-comms MCP table present",
1222
+ passed: !!mainTable,
1223
+ message: mainTable ? void 0 : `${MCP_SELECTOR} not found`
1224
+ });
1225
+ checks.push({
1226
+ name: "tap-comms env table present",
1227
+ passed: !!envTable,
1228
+ message: envTable ? void 0 : `${ENV_SELECTOR} not found`
1229
+ });
1230
+ for (const target of getTrustTargets(ctx)) {
1231
+ const selector = trustSelector(target);
1232
+ const trustTable = extractTomlTable(content, selector);
1233
+ checks.push({
1234
+ name: `Trust table present: ${canonicalizeTrustPath2(target)}`,
1235
+ passed: !!trustTable && trustTable.includes('trust_level = "trusted"'),
1236
+ message: trustTable && trustTable.includes('trust_level = "trusted"') ? void 0 : `${selector} missing trust_level = "trusted"`
1237
+ });
1238
+ }
1239
+ if (mainTable && managed.command) {
1240
+ checks.push({
1241
+ name: "Managed command configured",
1242
+ passed: mainTable.includes(
1243
+ `command = "${managed.command.replace(/\\/g, "\\\\")}"`
1244
+ ) && mainTable.includes(
1245
+ `args = ["${managed.args[0]?.replace(/\\/g, "\\\\") ?? ""}"]`
1246
+ ),
1247
+ message: "Managed tap-comms command/args do not match expected values"
1248
+ });
1249
+ }
1250
+ return checks;
1251
+ }
1252
+ var codexAdapter = {
1253
+ runtime: "codex",
1254
+ async probe(ctx) {
1255
+ const warnings = [];
1256
+ const issues = [];
1257
+ const configPath = findCodexConfigPath2();
1258
+ const configExists = fs9.existsSync(configPath);
1259
+ const runtimeProbe = probeCommand(
1260
+ ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
1261
+ );
1262
+ if (!runtimeProbe.command) {
1263
+ warnings.push(
1264
+ "Codex CLI not found in PATH. Config can still be written, but runtime verification will be limited."
1265
+ );
1266
+ }
1267
+ if (!fs9.existsSync(ctx.commsDir)) {
1268
+ issues.push(
1269
+ `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
1270
+ );
1271
+ }
1272
+ const managed = buildManagedMcpServerSpec(ctx);
1273
+ warnings.push(...managed.warnings);
1274
+ issues.push(...managed.issues);
1275
+ return {
1276
+ installed: true,
1277
+ configPath,
1278
+ configExists,
1279
+ runtimeCommand: runtimeProbe.command,
1280
+ version: runtimeProbe.version,
1281
+ canWrite: canWriteOrCreate(configPath),
1282
+ warnings,
1283
+ issues
1284
+ };
1285
+ },
1286
+ async plan(ctx, probe) {
1287
+ const configPath = probe.configPath ?? findCodexConfigPath2();
1288
+ const conflicts = [];
1289
+ const warnings = [];
1290
+ const operations = [];
1291
+ const ownedArtifacts = buildManagedArtifacts(configPath, ctx);
1292
+ if (probe.configExists) {
1293
+ const content = readConfigOrEmpty(configPath);
1294
+ if (extractTomlTable(content, MCP_SELECTOR)) {
1295
+ conflicts.push(`Existing ${MCP_SELECTOR} table will be updated.`);
1296
+ }
1297
+ if (extractTomlTable(content, ENV_SELECTOR)) {
1298
+ conflicts.push(`Existing ${ENV_SELECTOR} table will be updated.`);
1299
+ }
1300
+ for (const target of getTrustTargets(ctx)) {
1301
+ const selector = trustSelector(target);
1302
+ if (extractTomlTable(content, selector)) {
1303
+ conflicts.push(`Existing ${selector} table will be updated.`);
1304
+ }
1305
+ }
1306
+ }
1307
+ for (const artifact of ownedArtifacts) {
1308
+ operations.push({
1309
+ type: probe.configExists ? "merge" : "set",
1310
+ path: configPath,
1311
+ key: artifact.selector
1312
+ });
1313
+ }
1314
+ return {
1315
+ runtime: "codex",
1316
+ operations,
1317
+ ownedArtifacts,
1318
+ backupDir: ensureBackupDir(ctx.stateDir, "codex"),
1319
+ restartRequired: true,
1320
+ conflicts,
1321
+ warnings
1322
+ };
1323
+ },
1324
+ async apply(ctx, plan) {
1325
+ const configPath = plan.operations[0]?.path ?? findCodexConfigPath2();
1326
+ const warnings = [];
1327
+ const changedFiles = [];
1328
+ const managed = buildManagedMcpServerSpec(ctx);
1329
+ warnings.push(...managed.warnings);
1330
+ if (managed.issues.length > 0 || !managed.command) {
1331
+ return {
1332
+ success: false,
1333
+ appliedOps: 0,
1334
+ backupCreated: false,
1335
+ lastAppliedHash: "",
1336
+ ownedArtifacts: [],
1337
+ changedFiles,
1338
+ restartRequired: false,
1339
+ warnings: [...managed.warnings, ...managed.issues]
1340
+ };
1341
+ }
1342
+ const existingContent = readConfigOrEmpty(configPath);
1343
+ if (fs9.existsSync(configPath) && existingContent) {
1344
+ backupFile(configPath, plan.backupDir);
1345
+ }
1346
+ const artifactsWithBackups = plan.ownedArtifacts.map((artifact) => {
1347
+ const previousContent = artifact.kind === "toml-table" ? extractTomlTable(existingContent, artifact.selector) : null;
1348
+ const backupPath = artifactBackupPath(
1349
+ plan.backupDir,
1350
+ artifact.kind,
1351
+ artifact.selector
1352
+ );
1353
+ writeArtifactBackup(backupPath, {
1354
+ kind: "toml-table",
1355
+ selector: artifact.selector,
1356
+ existed: previousContent !== null,
1357
+ content: previousContent ?? void 0
1358
+ });
1359
+ return { ...artifact, backupPath };
1360
+ });
1361
+ let nextContent = existingContent;
1362
+ nextContent = replaceTomlTable(
1363
+ nextContent,
1364
+ MCP_SELECTOR,
1365
+ renderTomlTable(
1366
+ MCP_SELECTOR,
1367
+ {
1368
+ command: managed.command,
1369
+ args: managed.args
1370
+ },
1371
+ extractTomlTable(existingContent, MCP_SELECTOR)
1372
+ )
1373
+ );
1374
+ nextContent = replaceTomlTable(
1375
+ nextContent,
1376
+ ENV_SELECTOR,
1377
+ renderTomlTable(
1378
+ ENV_SELECTOR,
1379
+ managed.env,
1380
+ extractTomlTable(existingContent, ENV_SELECTOR)
1381
+ )
1382
+ );
1383
+ for (const target of getTrustTargets(ctx)) {
1384
+ const selector = trustSelector(target);
1385
+ nextContent = replaceTomlTable(
1386
+ nextContent,
1387
+ selector,
1388
+ renderTomlTable(
1389
+ selector,
1390
+ { trust_level: "trusted" },
1391
+ extractTomlTable(existingContent, selector)
1392
+ )
1393
+ );
1394
+ }
1395
+ writeTomlFile(configPath, nextContent);
1396
+ changedFiles.push(configPath);
1397
+ return {
1398
+ success: true,
1399
+ appliedOps: plan.operations.length,
1400
+ backupCreated: true,
1401
+ lastAppliedHash: fileHash(configPath),
1402
+ ownedArtifacts: artifactsWithBackups,
1403
+ changedFiles,
1404
+ restartRequired: true,
1405
+ warnings
1406
+ };
1407
+ },
1408
+ async verify(ctx, plan) {
1409
+ const warnings = [];
1410
+ const configPath = plan.operations[0]?.path ?? findCodexConfigPath2();
1411
+ const content = readConfigOrEmpty(configPath);
1412
+ const runtimeProbe = probeCommand(
1413
+ ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
1414
+ );
1415
+ const checks = verifyManagedToml(content, ctx, configPath);
1416
+ checks.push({
1417
+ name: "Comms directory exists",
1418
+ passed: fs9.existsSync(ctx.commsDir),
1419
+ message: fs9.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1420
+ });
1421
+ checks.push({
1422
+ name: "Codex CLI found",
1423
+ passed: !!runtimeProbe.command,
1424
+ message: runtimeProbe.command ? void 0 : "codex not in PATH (non-blocking)"
1425
+ });
1426
+ if (!runtimeProbe.command) {
1427
+ warnings.push(
1428
+ "Codex CLI not in PATH. Config is written, but runtime verification is partial."
1429
+ );
1430
+ }
1431
+ return {
1432
+ ok: checks.filter((check) => check.name !== "Codex CLI found").every((check) => check.passed),
1433
+ checks,
1434
+ restartRequired: true,
1435
+ warnings
1436
+ };
1437
+ },
1438
+ bridgeMode() {
1439
+ return "app-server";
1440
+ },
1441
+ resolveBridgeScript(ctx) {
1442
+ const distDir = path9.dirname(fileURLToPath(import.meta.url));
1443
+ const candidates = [
1444
+ // 1. Relative to bundled CLI (npm install / npx)
1445
+ path9.join(distDir, "bridges", "codex-bridge-runner.mjs"),
1446
+ // 2. Monorepo development — dist inside repo
1447
+ path9.join(
1448
+ ctx.repoRoot,
1449
+ "packages",
1450
+ "tap-comms",
1451
+ "dist",
1452
+ "bridges",
1453
+ "codex-bridge-runner.mjs"
1454
+ ),
1455
+ // 3. Source file — dev mode with strip-types
1456
+ path9.join(
1457
+ ctx.repoRoot,
1458
+ "packages",
1459
+ "tap-comms",
1460
+ "src",
1461
+ "bridges",
1462
+ "codex-bridge-runner.ts"
1463
+ )
1464
+ ];
1465
+ for (const candidate of candidates) {
1466
+ if (fs9.existsSync(candidate)) return candidate;
1467
+ }
1468
+ return null;
1469
+ }
1470
+ };
1471
+
1472
+ // src/adapters/gemini.ts
1473
+ import * as fs10 from "fs";
1474
+ import * as path10 from "path";
1475
+ var GEMINI_SELECTOR = "mcpServers.tap-comms";
1476
+ function candidateConfigPaths(ctx) {
1477
+ const home = getHomeDir();
1478
+ 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")
1482
+ ];
1483
+ }
1484
+ function chooseGeminiConfigPath(ctx) {
1485
+ 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();
1490
+ if (raw) {
1491
+ try {
1492
+ JSON.parse(raw);
1493
+ return antigravityConfig;
1494
+ } catch {
1495
+ }
1496
+ }
1497
+ }
1498
+ return workspaceConfig;
1499
+ }
1500
+ function readJsonFile(filePath) {
1501
+ if (!fs10.existsSync(filePath)) return {};
1502
+ const raw = fs10.readFileSync(filePath, "utf-8").trim();
1503
+ if (!raw) return {};
1504
+ return JSON.parse(raw);
1505
+ }
1506
+ function setNestedKey2(obj, keyPath, value) {
1507
+ const keys = keyPath.split(".");
1508
+ let current = obj;
1509
+ for (let i = 0; i < keys.length - 1; i++) {
1510
+ const key = keys[i];
1511
+ if (typeof current[key] !== "object" || current[key] === null) {
1512
+ current[key] = {};
1513
+ }
1514
+ current = current[key];
1515
+ }
1516
+ current[keys[keys.length - 1]] = value;
1517
+ }
1518
+ function readNestedKey(obj, keyPath) {
1519
+ let current = obj;
1520
+ for (const key of keyPath.split(".")) {
1521
+ if (typeof current !== "object" || current === null || !(key in current)) {
1522
+ return void 0;
1523
+ }
1524
+ current = current[key];
1525
+ }
1526
+ return current;
1527
+ }
1528
+ function verifyGeminiConfig(config, configPath, ctx) {
1529
+ const checks = [];
1530
+ const entry = readNestedKey(config, GEMINI_SELECTOR);
1531
+ checks.push({
1532
+ name: "Gemini config exists",
1533
+ passed: fs10.existsSync(configPath),
1534
+ message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
1535
+ });
1536
+ checks.push({
1537
+ name: "tap-comms entry present",
1538
+ passed: !!entry,
1539
+ message: entry ? void 0 : `${GEMINI_SELECTOR} not found`
1540
+ });
1541
+ checks.push({
1542
+ name: "Comms directory exists",
1543
+ passed: fs10.existsSync(ctx.commsDir),
1544
+ message: fs10.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1545
+ });
1546
+ if (entry?.env && typeof entry.env === "object") {
1547
+ checks.push({
1548
+ name: "TAP_COMMS_DIR configured",
1549
+ passed: entry.env.TAP_COMMS_DIR === ctx.commsDir.replace(/\\/g, "/"),
1550
+ message: `Expected ${ctx.commsDir.replace(/\\/g, "/")}`
1551
+ });
1552
+ }
1553
+ return checks;
1554
+ }
1555
+ var geminiAdapter = {
1556
+ runtime: "gemini",
1557
+ async probe(ctx) {
1558
+ const warnings = [];
1559
+ const issues = [];
1560
+ const configPath = chooseGeminiConfigPath(ctx);
1561
+ const configExists = fs10.existsSync(configPath);
1562
+ const runtimeProbe = probeCommand(
1563
+ ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
1564
+ );
1565
+ if (!runtimeProbe.command) {
1566
+ warnings.push(
1567
+ "Gemini CLI not found in PATH. Config can still be written, but runtime verification will be limited."
1568
+ );
1569
+ }
1570
+ if (!fs10.existsSync(ctx.commsDir)) {
1571
+ issues.push(`Comms directory not found: ${ctx.commsDir}. Run "init" first.`);
1572
+ }
1573
+ const managed = buildManagedMcpServerSpec(ctx);
1574
+ warnings.push(...managed.warnings);
1575
+ issues.push(...managed.issues);
1576
+ return {
1577
+ installed: true,
1578
+ configPath,
1579
+ configExists,
1580
+ runtimeCommand: runtimeProbe.command,
1581
+ version: runtimeProbe.version,
1582
+ canWrite: canWriteOrCreate(configPath),
1583
+ warnings,
1584
+ issues
1585
+ };
1586
+ },
1587
+ async plan(ctx, probe) {
1588
+ const configPath = probe.configPath ?? chooseGeminiConfigPath(ctx);
1589
+ const conflicts = [];
1590
+ const warnings = [];
1591
+ const operations = [];
1592
+ const ownedArtifacts = [
1593
+ { kind: "json-path", path: configPath, selector: GEMINI_SELECTOR }
1594
+ ];
1595
+ if (probe.configExists) {
1596
+ try {
1597
+ const config = readJsonFile(configPath);
1598
+ if (readNestedKey(config, GEMINI_SELECTOR) !== void 0) {
1599
+ conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
1600
+ }
1601
+ } catch {
1602
+ warnings.push(`${configPath} exists but is not valid JSON. It will be replaced.`);
1603
+ }
1604
+ }
1605
+ operations.push({
1606
+ type: probe.configExists ? "merge" : "set",
1607
+ path: configPath,
1608
+ key: GEMINI_SELECTOR
1609
+ });
1610
+ return {
1611
+ runtime: "gemini",
1612
+ operations,
1613
+ ownedArtifacts,
1614
+ backupDir: ensureBackupDir(ctx.stateDir, "gemini"),
1615
+ restartRequired: true,
1616
+ conflicts,
1617
+ warnings
1618
+ };
1619
+ },
1620
+ async apply(ctx, plan) {
1621
+ const configPath = plan.operations[0]?.path ?? chooseGeminiConfigPath(ctx);
1622
+ const warnings = [];
1623
+ const changedFiles = [];
1624
+ const managed = buildManagedMcpServerSpec(ctx);
1625
+ warnings.push(...managed.warnings);
1626
+ if (managed.issues.length > 0 || !managed.command) {
1627
+ return {
1628
+ success: false,
1629
+ appliedOps: 0,
1630
+ backupCreated: false,
1631
+ lastAppliedHash: "",
1632
+ ownedArtifacts: [],
1633
+ changedFiles,
1634
+ restartRequired: false,
1635
+ warnings: [...managed.warnings, ...managed.issues]
1636
+ };
1637
+ }
1638
+ let config = {};
1639
+ let previousValue = void 0;
1640
+ if (fs10.existsSync(configPath)) {
1641
+ if (fs10.readFileSync(configPath, "utf-8").trim()) {
1642
+ backupFile(configPath, plan.backupDir);
1643
+ }
1644
+ try {
1645
+ config = readJsonFile(configPath);
1646
+ } catch {
1647
+ warnings.push(`${configPath} was invalid JSON. Created backup and starting fresh.`);
1648
+ config = {};
1649
+ }
1650
+ previousValue = readNestedKey(config, GEMINI_SELECTOR);
1651
+ }
1652
+ const artifact = plan.ownedArtifacts[0];
1653
+ const backupPath = artifactBackupPath(plan.backupDir, artifact.kind, artifact.selector);
1654
+ writeArtifactBackup(backupPath, {
1655
+ kind: "json-path",
1656
+ selector: artifact.selector,
1657
+ existed: previousValue !== void 0,
1658
+ value: previousValue
1659
+ });
1660
+ setNestedKey2(config, GEMINI_SELECTOR, {
1661
+ command: managed.command,
1662
+ args: managed.args,
1663
+ env: managed.env
1664
+ });
1665
+ fs10.mkdirSync(path10.dirname(configPath), { recursive: true });
1666
+ const tmp = `${configPath}.tmp.${process.pid}`;
1667
+ fs10.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
1668
+ fs10.renameSync(tmp, configPath);
1669
+ changedFiles.push(configPath);
1670
+ return {
1671
+ success: true,
1672
+ appliedOps: plan.operations.length,
1673
+ backupCreated: true,
1674
+ lastAppliedHash: fileHash(configPath),
1675
+ ownedArtifacts: [{ ...artifact, backupPath }],
1676
+ changedFiles,
1677
+ restartRequired: true,
1678
+ warnings
1679
+ };
1680
+ },
1681
+ async verify(ctx, plan) {
1682
+ const warnings = [];
1683
+ const configPath = plan.operations[0]?.path ?? chooseGeminiConfigPath(ctx);
1684
+ const runtimeProbe = probeCommand(
1685
+ ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
1686
+ );
1687
+ let checks;
1688
+ try {
1689
+ const config = readJsonFile(configPath);
1690
+ checks = verifyGeminiConfig(config, configPath, ctx);
1691
+ } catch {
1692
+ checks = [
1693
+ {
1694
+ name: "Gemini config is valid JSON",
1695
+ passed: false,
1696
+ message: "Parse error"
1697
+ }
1698
+ ];
1699
+ }
1700
+ checks.push({
1701
+ name: "Gemini CLI found",
1702
+ passed: !!runtimeProbe.command,
1703
+ message: runtimeProbe.command ? void 0 : "gemini not in PATH (non-blocking)"
1704
+ });
1705
+ if (!runtimeProbe.command) {
1706
+ warnings.push(
1707
+ "Gemini CLI not in PATH. Config is written, but runtime verification is partial."
1708
+ );
1709
+ }
1710
+ return {
1711
+ ok: checks.filter((check) => check.name !== "Gemini CLI found").every((check) => check.passed),
1712
+ checks,
1713
+ restartRequired: true,
1714
+ warnings
1715
+ };
1716
+ },
1717
+ bridgeMode() {
1718
+ return "polling";
1719
+ }
1720
+ };
1721
+
1722
+ // src/adapters/index.ts
1723
+ var adapters = {
1724
+ claude: claudeAdapter,
1725
+ codex: codexAdapter,
1726
+ gemini: geminiAdapter
1727
+ };
1728
+ function getAdapter(runtime) {
1729
+ const adapter = adapters[runtime];
1730
+ if (!adapter) {
1731
+ throw new Error(
1732
+ `Adapter for "${runtime}" is not yet available. Supported: ${Object.keys(adapters).join(", ")}`
1733
+ );
1734
+ }
1735
+ return adapter;
1736
+ }
1737
+
1738
+ // 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";
1742
+
1743
+ // 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";
1747
+ function readNodeVersion(repoRoot) {
1748
+ const nvFile = path11.join(repoRoot, ".node-version");
1749
+ if (!fs11.existsSync(nvFile)) return null;
1750
+ try {
1751
+ const raw = fs11.readFileSync(nvFile, "utf-8").trim();
1752
+ return raw.length > 0 ? raw.replace(/^v/, "") : null;
1753
+ } catch {
1754
+ return null;
1755
+ }
1756
+ }
1757
+ function fnmCandidateDirs() {
1758
+ if (process.platform === "win32") {
1759
+ return [
1760
+ 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
1764
+ ].filter(Boolean);
1765
+ }
1766
+ return [
1767
+ 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
1771
+ ].filter(Boolean);
1772
+ }
1773
+ function nodeExecutableName() {
1774
+ return process.platform === "win32" ? "node.exe" : "node";
1775
+ }
1776
+ function probeFnmNode(desiredVersion) {
1777
+ const dirs = fnmCandidateDirs();
1778
+ const exe = nodeExecutableName();
1779
+ for (const baseDir of dirs) {
1780
+ const candidate = path11.join(
1781
+ baseDir,
1782
+ "node-versions",
1783
+ `v${desiredVersion}`,
1784
+ "installation",
1785
+ exe
1786
+ );
1787
+ if (!fs11.existsSync(candidate)) continue;
1788
+ try {
1789
+ const v = execSync2(`"${candidate}" --version`, {
1790
+ encoding: "utf-8",
1791
+ timeout: 5e3
1792
+ }).trim();
1793
+ if (v.startsWith(`v${desiredVersion.split(".")[0]}.`)) {
1794
+ return candidate;
1795
+ }
1796
+ } catch {
1797
+ }
1798
+ }
1799
+ return null;
1800
+ }
1801
+ function detectNodeMajorVersion(command) {
1802
+ try {
1803
+ const version2 = execSync2(`"${command}" --version`, {
1804
+ encoding: "utf-8",
1805
+ timeout: 5e3
1806
+ }).trim();
1807
+ const match = version2.match(/^v?(\d+)\./);
1808
+ return match ? parseInt(match[1], 10) : null;
1809
+ } catch {
1810
+ return null;
1811
+ }
1812
+ }
1813
+ function checkStripTypesSupport(command) {
1814
+ const major = detectNodeMajorVersion(command);
1815
+ if (major !== null && major >= 22) return true;
1816
+ try {
1817
+ execSync2(`"${command}" --experimental-strip-types -e ""`, {
1818
+ timeout: 5e3,
1819
+ stdio: "pipe"
1820
+ });
1821
+ return true;
1822
+ } catch {
1823
+ return false;
1824
+ }
1825
+ }
1826
+ function findTsxFallback(repoRoot) {
1827
+ 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")
1831
+ ];
1832
+ for (const c of candidates) {
1833
+ if (fs11.existsSync(c)) return c;
1834
+ }
1835
+ return null;
1836
+ }
1837
+ function getFnmBinDir(repoRoot) {
1838
+ const desiredVersion = readNodeVersion(repoRoot);
1839
+ if (!desiredVersion) return null;
1840
+ const nodePath = probeFnmNode(desiredVersion);
1841
+ if (!nodePath) return null;
1842
+ return path11.dirname(nodePath);
1843
+ }
1844
+ function resolveNodeRuntime(configCommand, repoRoot) {
1845
+ if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
1846
+ return {
1847
+ command: configCommand,
1848
+ supportsStripTypes: false,
1849
+ source: "bun",
1850
+ majorVersion: null
1851
+ };
1852
+ }
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
+ };
1864
+ }
1865
+ }
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
+ };
1874
+ }
1875
+ const tsx = findTsxFallback(repoRoot);
1876
+ if (tsx) {
1877
+ return {
1878
+ command: tsx,
1879
+ supportsStripTypes: false,
1880
+ source: "tsx-fallback",
1881
+ majorVersion: null
1882
+ };
1883
+ }
1884
+ 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}`
1899
+ };
1900
+ }
1901
+
1902
+ // src/engine/bridge.ts
1903
+ function pidFilePath(stateDir, instanceId) {
1904
+ return path12.join(stateDir, "pids", `bridge-${instanceId}.json`);
1905
+ }
1906
+ function logFilePath(stateDir, instanceId) {
1907
+ return path12.join(stateDir, "logs", `bridge-${instanceId}.log`);
1908
+ }
1909
+ function loadBridgeState(stateDir, instanceId) {
1910
+ const pidPath = pidFilePath(stateDir, instanceId);
1911
+ if (!fs12.existsSync(pidPath)) return null;
1912
+ try {
1913
+ const raw = fs12.readFileSync(pidPath, "utf-8");
1914
+ return JSON.parse(raw);
1915
+ } catch {
1916
+ return null;
1917
+ }
1918
+ }
1919
+ function saveBridgeState(stateDir, instanceId, state) {
1920
+ 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);
1925
+ }
1926
+ function clearBridgeState(stateDir, instanceId) {
1927
+ const pidPath = pidFilePath(stateDir, instanceId);
1928
+ if (fs12.existsSync(pidPath)) {
1929
+ fs12.unlinkSync(pidPath);
1930
+ }
1931
+ }
1932
+ function isProcessAlive(pid) {
1933
+ try {
1934
+ process.kill(pid, 0);
1935
+ return true;
1936
+ } catch {
1937
+ return false;
1938
+ }
1939
+ }
1940
+ function isBridgeRunning(stateDir, instanceId) {
1941
+ const state = loadBridgeState(stateDir, instanceId);
1942
+ if (!state) return false;
1943
+ return isProcessAlive(state.pid);
1944
+ }
1945
+ async function startBridge(options) {
1946
+ const {
1947
+ instanceId,
1948
+ runtime,
1949
+ stateDir,
1950
+ commsDir,
1951
+ bridgeScript,
1952
+ agentName,
1953
+ port
1954
+ } = options;
1955
+ const resolvedAgent = agentName || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
1956
+ if (!resolvedAgent) {
1957
+ throw new Error(
1958
+ `No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
1959
+ );
1960
+ }
1961
+ if (isBridgeRunning(stateDir, instanceId)) {
1962
+ const existing = loadBridgeState(stateDir, instanceId);
1963
+ throw new Error(
1964
+ `Bridge for ${instanceId} is already running (PID: ${existing.pid})`
1965
+ );
1966
+ }
1967
+ clearBridgeState(stateDir, instanceId);
1968
+ const logPath = logFilePath(stateDir, instanceId);
1969
+ fs12.mkdirSync(path12.dirname(logPath), { recursive: true });
1970
+ rotateLog(logPath);
1971
+ const logFd = fs12.openSync(logPath, "a");
1972
+ const repoRoot = options.repoRoot ?? path12.resolve(stateDir, "..");
1973
+ const resolved = resolveNodeRuntime(
1974
+ options.runtimeCommand ?? "node",
1975
+ repoRoot
1976
+ );
1977
+ const command = resolved.command;
1978
+ const runtimeEnv = buildRuntimeEnv(repoRoot);
1979
+ const child = spawn(command, [bridgeScript], {
1980
+ detached: true,
1981
+ stdio: ["ignore", logFd, logFd],
1982
+ env: {
1983
+ ...runtimeEnv,
1984
+ TAP_COMMS_DIR: commsDir,
1985
+ TAP_BRIDGE_RUNTIME: runtime,
1986
+ TAP_BRIDGE_INSTANCE_ID: instanceId,
1987
+ TAP_AGENT_NAME: resolvedAgent,
1988
+ CODEX_TAP_AGENT_NAME: resolvedAgent,
1989
+ TAP_RESOLVED_NODE: resolved.command,
1990
+ TAP_STRIP_TYPES: resolved.supportsStripTypes ? "1" : "0",
1991
+ ...options.appServerUrl ? { CODEX_APP_SERVER_URL: options.appServerUrl } : {},
1992
+ ...port != null ? { TAP_BRIDGE_PORT: String(port) } : {},
1993
+ ...options.headless?.enabled ? {
1994
+ TAP_HEADLESS: "true",
1995
+ TAP_AGENT_ROLE: options.headless.role,
1996
+ TAP_MAX_REVIEW_ROUNDS: String(options.headless.maxRounds),
1997
+ TAP_QUALITY_FLOOR: options.headless.qualitySeverityFloor
1998
+ } : {},
1999
+ // Bridge script operational flags
2000
+ ...options.busyMode ? { TAP_BUSY_MODE: options.busyMode } : {},
2001
+ ...options.pollSeconds != null ? { TAP_POLL_SECONDS: String(options.pollSeconds) } : {},
2002
+ ...options.reconnectSeconds != null ? { TAP_RECONNECT_SECONDS: String(options.reconnectSeconds) } : {},
2003
+ ...options.messageLookbackMinutes != null ? {
2004
+ TAP_MESSAGE_LOOKBACK_MINUTES: String(
2005
+ options.messageLookbackMinutes
2006
+ )
2007
+ } : {},
2008
+ ...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
2009
+ ...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
2010
+ ...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
2011
+ }
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;
2025
+ }
2026
+ async function stopBridge(options) {
2027
+ const { instanceId, stateDir, platform } = options;
2028
+ const state = loadBridgeState(stateDir, instanceId);
2029
+ if (!state) {
2030
+ return false;
2031
+ }
2032
+ if (!isProcessAlive(state.pid)) {
2033
+ clearBridgeState(stateDir, instanceId);
2034
+ return false;
2035
+ }
2036
+ 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
+ }
2046
+ } catch {
2047
+ }
2048
+ clearBridgeState(stateDir, instanceId);
2049
+ return true;
2050
+ }
2051
+ function rotateLog(logPath) {
2052
+ if (!fs12.existsSync(logPath)) return;
2053
+ try {
2054
+ const stats = fs12.statSync(logPath);
2055
+ if (stats.size === 0) return;
2056
+ const prevPath = `${logPath}.prev`;
2057
+ fs12.renameSync(logPath, prevPath);
2058
+ } catch {
2059
+ }
2060
+ }
2061
+ function getHeartbeatAge(stateDir, instanceId) {
2062
+ const state = loadBridgeState(stateDir, instanceId);
2063
+ if (!state?.lastHeartbeat) return null;
2064
+ const heartbeatTime = new Date(state.lastHeartbeat).getTime();
2065
+ if (isNaN(heartbeatTime)) return null;
2066
+ return Math.floor((Date.now() - heartbeatTime) / 1e3);
2067
+ }
2068
+ function getBridgeStatus(stateDir, instanceId) {
2069
+ const state = loadBridgeState(stateDir, instanceId);
2070
+ if (!state) return "stopped";
2071
+ if (!isProcessAlive(state.pid)) {
2072
+ clearBridgeState(stateDir, instanceId);
2073
+ return "stale";
2074
+ }
2075
+ return "running";
2076
+ }
2077
+
2078
+ // src/commands/add.ts
2079
+ async function addCommand(args) {
2080
+ const { positional, flags } = parseArgs(args);
2081
+ const runtimeArg = positional[0];
2082
+ if (!runtimeArg) {
2083
+ return {
2084
+ ok: false,
2085
+ command: "add",
2086
+ 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>]",
2088
+ warnings: [],
2089
+ data: {}
2090
+ };
2091
+ }
2092
+ if (!isValidRuntime(runtimeArg)) {
2093
+ return {
2094
+ ok: false,
2095
+ command: "add",
2096
+ code: "TAP_RUNTIME_UNKNOWN",
2097
+ message: `Unknown runtime: ${runtimeArg}. Available: claude, codex, gemini`,
2098
+ warnings: [],
2099
+ data: {}
2100
+ };
2101
+ }
2102
+ const runtime = runtimeArg;
2103
+ const instanceName = typeof flags["name"] === "string" ? flags["name"] : void 0;
2104
+ const instanceId = buildInstanceId(runtime, instanceName);
2105
+ const portStr = typeof flags["port"] === "string" ? flags["port"] : void 0;
2106
+ const port = portStr ? parseInt(portStr, 10) : null;
2107
+ const force = flags["force"] === true;
2108
+ const headlessFlag = flags["headless"] === true;
2109
+ const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
2110
+ const validRoles = ["reviewer", "validator", "long-running"];
2111
+ if (roleArg && !validRoles.includes(roleArg)) {
2112
+ return {
2113
+ ok: false,
2114
+ command: "add",
2115
+ runtime,
2116
+ instanceId,
2117
+ code: "TAP_INVALID_ARGUMENT",
2118
+ message: `Invalid role: ${roleArg}. Available: ${validRoles.join(", ")}`,
2119
+ warnings: [],
2120
+ data: {}
2121
+ };
2122
+ }
2123
+ if (headlessFlag && !instanceName) {
2124
+ return {
2125
+ ok: false,
2126
+ command: "add",
2127
+ runtime,
2128
+ instanceId,
2129
+ code: "TAP_INVALID_ARGUMENT",
2130
+ message: "--headless requires --name for instance isolation",
2131
+ warnings: [],
2132
+ data: {}
2133
+ };
2134
+ }
2135
+ const headless = headlessFlag ? {
2136
+ enabled: true,
2137
+ role: roleArg ?? "reviewer",
2138
+ maxRounds: 5,
2139
+ qualitySeverityFloor: "high"
2140
+ } : null;
2141
+ if (portStr && (port === null || isNaN(port))) {
2142
+ return {
2143
+ ok: false,
2144
+ command: "add",
2145
+ runtime,
2146
+ instanceId,
2147
+ code: "TAP_INVALID_ARGUMENT",
2148
+ message: `Invalid port: ${portStr}`,
2149
+ warnings: [],
2150
+ data: {}
2151
+ };
2152
+ }
2153
+ const repoRoot = findRepoRoot2();
2154
+ const state = loadState(repoRoot);
2155
+ if (!state) {
2156
+ return {
2157
+ ok: false,
2158
+ command: "add",
2159
+ runtime,
2160
+ instanceId,
2161
+ code: "TAP_NOT_INITIALIZED",
2162
+ message: "Not initialized. Run: npx @hua-labs/tap init",
2163
+ warnings: [],
2164
+ data: {}
2165
+ };
2166
+ }
2167
+ if (state.instances[instanceId]?.installed && !force) {
2168
+ return {
2169
+ ok: true,
2170
+ command: "add",
2171
+ runtime,
2172
+ instanceId,
2173
+ code: "TAP_NO_OP",
2174
+ message: `${instanceId} is already installed. Use --force to re-install.`,
2175
+ warnings: [],
2176
+ data: {}
2177
+ };
2178
+ }
2179
+ if (port !== null) {
2180
+ const conflict = findPortConflict(state, port, instanceId);
2181
+ if (conflict) {
2182
+ return {
2183
+ ok: false,
2184
+ command: "add",
2185
+ runtime,
2186
+ instanceId,
2187
+ code: "TAP_PORT_CONFLICT",
2188
+ message: `Port ${port} is already used by instance "${conflict}".`,
2189
+ warnings: [],
2190
+ data: { conflictingInstance: conflict }
2191
+ };
2192
+ }
2193
+ }
2194
+ logHeader(`@hua-labs/tap add ${instanceId}`);
2195
+ if (instanceName) log(`Instance name: ${instanceName}`);
2196
+ if (port !== null) log(`Port: ${port}`);
2197
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
2198
+ const adapter = getAdapter(runtime);
2199
+ const warnings = [];
2200
+ log("Probing runtime...");
2201
+ const probe = await adapter.probe(ctx);
2202
+ if (!probe.installed) {
2203
+ return {
2204
+ ok: false,
2205
+ command: "add",
2206
+ runtime,
2207
+ instanceId,
2208
+ code: "TAP_RUNTIME_NOT_FOUND",
2209
+ message: `${runtime} runtime not found.`,
2210
+ warnings: probe.warnings,
2211
+ data: { issues: probe.issues }
2212
+ };
2213
+ }
2214
+ logSuccess(`Found ${runtime} (${probe.runtimeCommand ?? "unknown"})`);
2215
+ if (probe.configPath) log(`Config: ${probe.configPath}`);
2216
+ warnings.push(...probe.warnings);
2217
+ for (const w of probe.warnings) logWarn(w);
2218
+ log("Planning patches...");
2219
+ const plan = await adapter.plan(ctx, probe);
2220
+ warnings.push(...plan.warnings);
2221
+ if (plan.conflicts.length > 0) {
2222
+ logWarn("Conflicts detected:");
2223
+ for (const c of plan.conflicts) logWarn(` ${c}`);
2224
+ }
2225
+ log(`Operations: ${plan.operations.length}`);
2226
+ log(`Artifacts: ${plan.ownedArtifacts.length}`);
2227
+ for (const w of plan.warnings) logWarn(w);
2228
+ if (plan.operations.length === 0) {
2229
+ return {
2230
+ ok: true,
2231
+ command: "add",
2232
+ runtime,
2233
+ instanceId,
2234
+ code: "TAP_NO_OP",
2235
+ message: "No operations to apply. Runtime not configured.",
2236
+ warnings,
2237
+ data: { planOps: 0 }
2238
+ };
2239
+ }
2240
+ const backupDir = ensureBackupDir(ctx.stateDir, instanceId);
2241
+ log(`Backup dir: ${backupDir}`);
2242
+ log("Applying patches...");
2243
+ const result = await adapter.apply(ctx, plan);
2244
+ warnings.push(...result.warnings);
2245
+ if (!result.success) {
2246
+ return {
2247
+ ok: false,
2248
+ command: "add",
2249
+ runtime,
2250
+ instanceId,
2251
+ code: "TAP_PATCH_FAILED",
2252
+ message: "Failed to apply patches.",
2253
+ warnings,
2254
+ data: { appliedOps: result.appliedOps }
2255
+ };
2256
+ }
2257
+ logSuccess(`Applied ${result.appliedOps} operation(s)`);
2258
+ for (const f of result.changedFiles) logSuccess(`Modified: ${f}`);
2259
+ for (const w of result.warnings) logWarn(w);
2260
+ log("Verifying...");
2261
+ const verify = await adapter.verify(ctx, plan);
2262
+ warnings.push(...verify.warnings);
2263
+ for (const check of verify.checks) {
2264
+ if (check.passed) {
2265
+ logSuccess(`${check.name}`);
2266
+ } else {
2267
+ logError(`${check.name}: ${check.message ?? "failed"}`);
2268
+ }
2269
+ }
2270
+ if (!verify.ok) {
2271
+ logWarn(
2272
+ "Verification had failures. Runtime may need manual configuration."
2273
+ );
2274
+ }
2275
+ let bridge = null;
2276
+ const mode = adapter.bridgeMode();
2277
+ if (mode === "app-server") {
2278
+ const bridgeScript = adapter.resolveBridgeScript?.(ctx);
2279
+ if (!bridgeScript) {
2280
+ logWarn("Bridge script not found. Bridge not started.");
2281
+ warnings.push("Bridge script not found. Run bridge manually.");
2282
+ } else {
2283
+ const agentNameEnv = process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
2284
+ if (!agentNameEnv) {
2285
+ logWarn(
2286
+ "No agent name set (TAP_AGENT_NAME). Bridge not started. Use: npx @hua-labs/tap bridge start <instance> --agent-name <name>"
2287
+ );
2288
+ warnings.push("Bridge not auto-started: no agent name available.");
2289
+ } else {
2290
+ const { config: resolvedCfg } = resolveConfig({}, repoRoot);
2291
+ log(`Starting bridge: ${bridgeScript}`);
2292
+ try {
2293
+ bridge = await startBridge({
2294
+ instanceId,
2295
+ runtime,
2296
+ stateDir: ctx.stateDir,
2297
+ commsDir: ctx.commsDir,
2298
+ bridgeScript,
2299
+ platform: ctx.platform,
2300
+ agentName: agentNameEnv,
2301
+ runtimeCommand: resolvedCfg.runtimeCommand,
2302
+ appServerUrl: resolvedCfg.appServerUrl,
2303
+ repoRoot,
2304
+ port: port ?? void 0,
2305
+ headless
2306
+ });
2307
+ logSuccess(`Bridge started (PID: ${bridge.pid})`);
2308
+ } catch (err) {
2309
+ const msg = err instanceof Error ? err.message : String(err);
2310
+ logWarn(`Bridge not started: ${msg}`);
2311
+ warnings.push(`Bridge not started: ${msg}`);
2312
+ }
2313
+ }
2314
+ }
2315
+ }
2316
+ const instanceState = {
2317
+ instanceId,
2318
+ runtime,
2319
+ agentName: null,
2320
+ port,
2321
+ installed: true,
2322
+ configPath: probe.configPath ?? "",
2323
+ bridgeMode: mode,
2324
+ restartRequired: result.restartRequired,
2325
+ ownedArtifacts: result.ownedArtifacts,
2326
+ backupPath: backupDir,
2327
+ lastAppliedHash: result.lastAppliedHash,
2328
+ lastVerifiedAt: verify.ok ? (/* @__PURE__ */ new Date()).toISOString() : null,
2329
+ bridge,
2330
+ headless,
2331
+ warnings: [...result.warnings, ...verify.warnings]
2332
+ };
2333
+ const newState = updateInstanceState(state, instanceId, instanceState);
2334
+ saveState(repoRoot, newState);
2335
+ logSuccess("State saved");
2336
+ if (result.restartRequired) {
2337
+ logWarn(`Restart ${runtime} to pick up the new configuration.`);
2338
+ }
2339
+ logHeader("Done!");
2340
+ return {
2341
+ ok: true,
2342
+ command: "add",
2343
+ runtime,
2344
+ instanceId,
2345
+ code: "TAP_ADD_OK",
2346
+ message: `${instanceId} configured`,
2347
+ warnings,
2348
+ data: {
2349
+ appliedOps: result.appliedOps,
2350
+ restartRequired: result.restartRequired,
2351
+ changedFiles: result.changedFiles,
2352
+ verified: verify.ok
2353
+ }
2354
+ };
2355
+ }
2356
+
2357
+ // src/commands/status.ts
2358
+ function resolveStatus(inst, stateDir) {
2359
+ if (!inst.installed) return "not installed";
2360
+ switch (inst.bridgeMode) {
2361
+ case "native-push":
2362
+ case "polling":
2363
+ return inst.lastVerifiedAt ? "active" : "configured";
2364
+ case "app-server":
2365
+ if (inst.bridge && isBridgeRunning(stateDir, inst.instanceId)) {
2366
+ return "active";
2367
+ }
2368
+ if (inst.bridge) {
2369
+ inst.bridge = null;
2370
+ }
2371
+ return inst.lastVerifiedAt ? "configured" : "installed";
2372
+ default:
2373
+ return "installed";
2374
+ }
2375
+ }
2376
+ function instanceStatusLine(inst, status) {
2377
+ const bridgeInfo = inst.bridge ? ` (pid: ${inst.bridge.pid})` : "";
2378
+ const mode = inst.bridgeMode;
2379
+ const portStr = inst.port ? ` port:${inst.port}` : "";
2380
+ const restart = inst.restartRequired ? " [restart required]" : "";
2381
+ const warns = inst.warnings.length > 0 ? ` [${inst.warnings.length} warning(s)]` : "";
2382
+ return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
2383
+ }
2384
+ async function statusCommand(_args) {
2385
+ const repoRoot = findRepoRoot2();
2386
+ const state = loadState(repoRoot);
2387
+ if (!state) {
2388
+ return {
2389
+ ok: false,
2390
+ command: "status",
2391
+ code: "TAP_NOT_INITIALIZED",
2392
+ message: "Not initialized. Run: npx @hua-labs/tap init",
2393
+ warnings: [],
2394
+ data: {}
2395
+ };
2396
+ }
2397
+ logHeader("@hua-labs/tap status");
2398
+ log(`Version: ${state.packageVersion}`);
2399
+ log(`Comms dir: ${state.commsDir}`);
2400
+ log(`Repo root: ${state.repoRoot}`);
2401
+ log(`Schema: v${state.schemaVersion}`);
2402
+ log(`Updated: ${state.updatedAt}`);
2403
+ const installed = getInstalledInstances(state);
2404
+ const { config: resolvedCfg } = resolveConfig({}, repoRoot);
2405
+ const stateDir = resolvedCfg.stateDir;
2406
+ const instances = {};
2407
+ const bridgesBefore = installed.map((id) => state.instances[id]?.bridge);
2408
+ if (installed.length === 0) {
2409
+ log("");
2410
+ log("No instances installed.");
2411
+ log("Run: npx @hua-labs/tap add <claude|codex|gemini>");
2412
+ } else {
2413
+ log("");
2414
+ log(
2415
+ `${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(14)} ${"Bridge Mode".padEnd(14)} Details`
2416
+ );
2417
+ log(
2418
+ `${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(14)} ${"\u2500".repeat(14)} ${"\u2500".repeat(20)}`
2419
+ );
2420
+ for (const id of installed) {
2421
+ const inst = state.instances[id];
2422
+ if (inst) {
2423
+ const status = resolveStatus(inst, stateDir);
2424
+ log(instanceStatusLine(inst, status));
2425
+ if (inst.warnings.length > 0) {
2426
+ for (const w of inst.warnings) {
2427
+ logWarn(` ${w}`);
2428
+ }
2429
+ }
2430
+ instances[id] = {
2431
+ status,
2432
+ runtime: inst.runtime,
2433
+ bridgeMode: inst.bridgeMode,
2434
+ bridge: inst.bridge,
2435
+ port: inst.port,
2436
+ warnings: inst.warnings
2437
+ };
2438
+ }
2439
+ }
2440
+ }
2441
+ const bridgesAfter = installed.map((id) => state.instances[id]?.bridge);
2442
+ const staleCleared = bridgesBefore.some((b, i) => b !== bridgesAfter[i]);
2443
+ if (staleCleared) {
2444
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2445
+ saveState(repoRoot, state);
2446
+ }
2447
+ log("");
2448
+ return {
2449
+ ok: true,
2450
+ command: "status",
2451
+ code: "TAP_STATUS_OK",
2452
+ message: `${installed.length} instance(s) installed`,
2453
+ warnings: [],
2454
+ data: {
2455
+ version: state.packageVersion,
2456
+ commsDir: state.commsDir,
2457
+ repoRoot: state.repoRoot,
2458
+ instances
2459
+ }
2460
+ };
2461
+ }
2462
+
2463
+ // src/engine/rollback.ts
2464
+ import * as fs13 from "fs";
2465
+ async function rollbackRuntime(_instanceId, runtimeState) {
2466
+ const errors = [];
2467
+ const restoredFiles = [];
2468
+ let restoredCount = 0;
2469
+ for (const artifact of runtimeState.ownedArtifacts) {
2470
+ try {
2471
+ const result = rollbackArtifact(artifact);
2472
+ if (result.restored) {
2473
+ restoredCount++;
2474
+ restoredFiles.push(artifact.path);
2475
+ }
2476
+ if (result.error) {
2477
+ errors.push(result.error);
2478
+ }
2479
+ } catch (err) {
2480
+ errors.push(
2481
+ `Failed to rollback ${artifact.path}#${artifact.selector}: ${err instanceof Error ? err.message : String(err)}`
2482
+ );
2483
+ }
2484
+ }
2485
+ return {
2486
+ success: errors.length === 0,
2487
+ restoredCount,
2488
+ restoredFiles,
2489
+ errors
2490
+ };
2491
+ }
2492
+ function rollbackArtifact(artifact) {
2493
+ if (!fs13.existsSync(artifact.path)) {
2494
+ return { restored: false, error: `File not found: ${artifact.path}` };
2495
+ }
2496
+ switch (artifact.kind) {
2497
+ case "json-path":
2498
+ return rollbackJsonPath(artifact);
2499
+ case "toml-table":
2500
+ return rollbackTomlTable(artifact);
2501
+ case "file":
2502
+ return rollbackFile(artifact);
2503
+ default:
2504
+ return {
2505
+ restored: false,
2506
+ error: `Unknown artifact kind: ${artifact.kind}`
2507
+ };
2508
+ }
2509
+ }
2510
+ function rollbackJsonPath(artifact) {
2511
+ const raw = fs13.readFileSync(artifact.path, "utf-8");
2512
+ let config;
2513
+ try {
2514
+ config = JSON.parse(raw);
2515
+ } catch {
2516
+ return { restored: false, error: `Invalid JSON: ${artifact.path}` };
2517
+ }
2518
+ const backup = artifact.backupPath ? readArtifactBackup(artifact.backupPath) : null;
2519
+ if (backup?.kind === "json-path" && backup.selector === artifact.selector) {
2520
+ if (backup.existed) {
2521
+ setNestedKey3(config, artifact.selector, backup.value);
2522
+ } else {
2523
+ deleteNestedKey(config, artifact.selector);
2524
+ cleanEmptyParents(config, artifact.selector);
2525
+ }
2526
+ } else {
2527
+ const removed = deleteNestedKey(config, artifact.selector);
2528
+ if (!removed) {
2529
+ return {
2530
+ restored: false,
2531
+ error: `Key not found: ${artifact.selector} in ${artifact.path}`
2532
+ };
2533
+ }
2534
+ cleanEmptyParents(config, artifact.selector);
2535
+ }
2536
+ 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);
2539
+ return { restored: true };
2540
+ }
2541
+ function rollbackTomlTable(artifact) {
2542
+ const content = fs13.readFileSync(artifact.path, "utf-8");
2543
+ const backup = artifact.backupPath ? readArtifactBackup(artifact.backupPath) : null;
2544
+ if (backup?.kind === "toml-table" && backup.selector === artifact.selector) {
2545
+ const nextContent = backup.existed ? replaceTomlTable(content, artifact.selector, backup.content ?? "") : removeTomlTable(content, artifact.selector);
2546
+ const tmp2 = `${artifact.path}.tmp.${process.pid}`;
2547
+ fs13.writeFileSync(tmp2, nextContent, "utf-8");
2548
+ fs13.renameSync(tmp2, artifact.path);
2549
+ return { restored: true };
2550
+ }
2551
+ if (!extractTomlTable(content, artifact.selector)) {
2552
+ return {
2553
+ restored: false,
2554
+ error: `TOML table not found: ${artifact.selector}`
2555
+ };
2556
+ }
2557
+ const tmp = `${artifact.path}.tmp.${process.pid}`;
2558
+ fs13.writeFileSync(tmp, removeTomlTable(content, artifact.selector), "utf-8");
2559
+ fs13.renameSync(tmp, artifact.path);
2560
+ return { restored: true };
2561
+ }
2562
+ function rollbackFile(artifact) {
2563
+ if (fs13.existsSync(artifact.path)) {
2564
+ fs13.unlinkSync(artifact.path);
2565
+ return { restored: true };
2566
+ }
2567
+ return { restored: false, error: `File not found: ${artifact.path}` };
2568
+ }
2569
+ function deleteNestedKey(obj, keyPath) {
2570
+ const keys = keyPath.split(".");
2571
+ let current = obj;
2572
+ for (let i = 0; i < keys.length - 1; i++) {
2573
+ const key = keys[i];
2574
+ if (typeof current[key] !== "object" || current[key] === null) {
2575
+ return false;
2576
+ }
2577
+ current = current[key];
2578
+ }
2579
+ const lastKey = keys[keys.length - 1];
2580
+ if (!(lastKey in current)) return false;
2581
+ delete current[lastKey];
2582
+ return true;
2583
+ }
2584
+ function setNestedKey3(obj, keyPath, value) {
2585
+ const keys = keyPath.split(".");
2586
+ let current = obj;
2587
+ for (let i = 0; i < keys.length - 1; i++) {
2588
+ const key = keys[i];
2589
+ if (typeof current[key] !== "object" || current[key] === null) {
2590
+ current[key] = {};
2591
+ }
2592
+ current = current[key];
2593
+ }
2594
+ current[keys[keys.length - 1]] = value;
2595
+ }
2596
+ function cleanEmptyParents(obj, keyPath) {
2597
+ const keys = keyPath.split(".");
2598
+ for (let depth = keys.length - 2; depth >= 0; depth--) {
2599
+ let current = obj;
2600
+ for (let i = 0; i < depth; i++) {
2601
+ current = current[keys[i]];
2602
+ if (!current) return;
2603
+ }
2604
+ const key = keys[depth];
2605
+ const value = current[key];
2606
+ if (typeof value === "object" && value !== null && Object.keys(value).length === 0) {
2607
+ delete current[key];
2608
+ }
2609
+ }
2610
+ }
2611
+
2612
+ // src/commands/remove.ts
2613
+ async function removeCommand(args) {
2614
+ const identifier = args.find((a) => !a.startsWith("-"));
2615
+ if (!identifier) {
2616
+ return {
2617
+ ok: false,
2618
+ command: "remove",
2619
+ code: "TAP_INVALID_ARGUMENT",
2620
+ message: "Missing instance argument. Usage: npx @hua-labs/tap remove <instance>",
2621
+ warnings: [],
2622
+ data: {}
2623
+ };
2624
+ }
2625
+ const repoRoot = findRepoRoot2();
2626
+ const state = loadState(repoRoot);
2627
+ if (!state) {
2628
+ return {
2629
+ ok: false,
2630
+ command: "remove",
2631
+ code: "TAP_NOT_INITIALIZED",
2632
+ message: "Not initialized. Run: npx @hua-labs/tap init",
2633
+ warnings: [],
2634
+ data: {}
2635
+ };
2636
+ }
2637
+ const resolved = resolveInstanceId(identifier, state);
2638
+ if (!resolved.ok) {
2639
+ return {
2640
+ ok: false,
2641
+ command: "remove",
2642
+ code: resolved.code,
2643
+ message: resolved.message,
2644
+ warnings: [],
2645
+ data: {}
2646
+ };
2647
+ }
2648
+ const instanceId = resolved.instanceId;
2649
+ const instance = state.instances[instanceId];
2650
+ if (!instance?.installed) {
2651
+ return {
2652
+ ok: true,
2653
+ command: "remove",
2654
+ instanceId,
2655
+ code: "TAP_NO_OP",
2656
+ message: `${instanceId} is not installed.`,
2657
+ warnings: [],
2658
+ data: {}
2659
+ };
2660
+ }
2661
+ logHeader(`@hua-labs/tap remove ${instanceId}`);
2662
+ if (instance.bridge) {
2663
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
2664
+ const stopped = await stopBridge({
2665
+ instanceId,
2666
+ stateDir: ctx.stateDir,
2667
+ platform: ctx.platform
2668
+ });
2669
+ if (stopped) {
2670
+ logSuccess(`Bridge for ${instanceId} stopped`);
2671
+ } else {
2672
+ log(`No running bridge for ${instanceId}`);
2673
+ }
2674
+ }
2675
+ const result = await rollbackRuntime(instanceId, instance);
2676
+ if (result.success) {
2677
+ logSuccess(`Rolled back ${result.restoredCount} artifact(s)`);
2678
+ for (const f of result.restoredFiles) logSuccess(`Restored: ${f}`);
2679
+ const newState = removeInstanceState(state, instanceId);
2680
+ saveState(repoRoot, newState);
2681
+ logSuccess("State updated");
2682
+ logHeader("Done!");
2683
+ return {
2684
+ ok: true,
2685
+ command: "remove",
2686
+ instanceId,
2687
+ runtime: instance.runtime,
2688
+ code: "TAP_REMOVE_OK",
2689
+ message: `${instanceId} removed successfully`,
2690
+ warnings: [],
2691
+ data: {
2692
+ restoredCount: result.restoredCount,
2693
+ restoredFiles: result.restoredFiles
2694
+ }
2695
+ };
2696
+ }
2697
+ for (const e of result.errors) logError(e);
2698
+ return {
2699
+ ok: false,
2700
+ command: "remove",
2701
+ instanceId,
2702
+ runtime: instance.runtime,
2703
+ code: "TAP_ROLLBACK_FAILED",
2704
+ message: "Rollback had errors. State preserved for retry.",
2705
+ warnings: result.errors,
2706
+ data: { restoredCount: result.restoredCount }
2707
+ };
2708
+ }
2709
+
2710
+ // src/commands/bridge.ts
2711
+ import * as path13 from "path";
2712
+ function formatAge(seconds) {
2713
+ if (seconds < 60) return `${seconds}s ago`;
2714
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
2715
+ return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
2716
+ }
2717
+ var BRIDGE_HELP = `
2718
+ Usage:
2719
+ tap-comms bridge <subcommand> [instance] [options]
2720
+
2721
+ Subcommands:
2722
+ start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
2723
+ stop <instance> Stop bridge for an instance
2724
+ stop Stop all running bridges
2725
+ status Show bridge status for all instances
2726
+ status <instance> Show bridge status for a specific instance
2727
+
2728
+ Options:
2729
+ --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
2730
+ --busy-mode <steer|wait> How to handle active turns (default: steer)
2731
+ --poll-seconds <n> Inbox poll interval (default: 5)
2732
+ --reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
2733
+ --message-lookback-minutes <n> Process messages from last N minutes (default: 10)
2734
+ --thread-id <id> Resume specific thread
2735
+ --ephemeral Use ephemeral thread (no persistence)
2736
+ --process-existing-messages Process all existing inbox messages
2737
+
2738
+ Examples:
2739
+ npx @hua-labs/tap bridge start codex --agent-name myAgent
2740
+ npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer --busy-mode steer
2741
+ npx @hua-labs/tap bridge stop codex
2742
+ npx @hua-labs/tap bridge stop
2743
+ npx @hua-labs/tap bridge status
2744
+ `.trim();
2745
+ async function bridgeStart(identifier, agentName, flags = {}) {
2746
+ const repoRoot = findRepoRoot2();
2747
+ const state = loadState(repoRoot);
2748
+ if (!state) {
2749
+ return {
2750
+ ok: false,
2751
+ command: "bridge",
2752
+ code: "TAP_NOT_INITIALIZED",
2753
+ message: "Not initialized. Run: npx @hua-labs/tap init",
2754
+ warnings: [],
2755
+ data: {}
2756
+ };
2757
+ }
2758
+ const resolved = resolveInstanceId(identifier, state);
2759
+ if (!resolved.ok) {
2760
+ return {
2761
+ ok: false,
2762
+ command: "bridge",
2763
+ code: resolved.code,
2764
+ message: resolved.message,
2765
+ warnings: [],
2766
+ data: {}
2767
+ };
2768
+ }
2769
+ const instanceId = resolved.instanceId;
2770
+ const instance = state.instances[instanceId];
2771
+ if (!instance?.installed) {
2772
+ return {
2773
+ ok: false,
2774
+ command: "bridge",
2775
+ instanceId,
2776
+ runtime: instance?.runtime,
2777
+ code: "TAP_INSTANCE_NOT_FOUND",
2778
+ message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance?.runtime ?? identifier}`,
2779
+ warnings: [],
2780
+ data: {}
2781
+ };
2782
+ }
2783
+ const adapter = getAdapter(instance.runtime);
2784
+ const mode = adapter.bridgeMode();
2785
+ if (mode !== "app-server") {
2786
+ return {
2787
+ ok: true,
2788
+ command: "bridge",
2789
+ instanceId,
2790
+ runtime: instance.runtime,
2791
+ code: "TAP_NO_OP",
2792
+ message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
2793
+ warnings: [],
2794
+ data: { bridgeMode: mode }
2795
+ };
2796
+ }
2797
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
2798
+ const bridgeScript = adapter.resolveBridgeScript?.(ctx);
2799
+ if (!bridgeScript) {
2800
+ return {
2801
+ ok: false,
2802
+ command: "bridge",
2803
+ instanceId,
2804
+ runtime: instance.runtime,
2805
+ code: "TAP_BRIDGE_SCRIPT_MISSING",
2806
+ message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
2807
+ warnings: [],
2808
+ data: {}
2809
+ };
2810
+ }
2811
+ const { config: resolvedConfig } = resolveConfig({}, repoRoot);
2812
+ const runtimeCommand = resolvedConfig.runtimeCommand;
2813
+ const appServerUrl = resolvedConfig.appServerUrl;
2814
+ logHeader(`@hua-labs/tap bridge start ${instanceId}`);
2815
+ log(`Bridge script: ${bridgeScript}`);
2816
+ log(`Bridge mode: ${mode}`);
2817
+ log(`Runtime cmd: ${runtimeCommand}`);
2818
+ log(`App server: ${appServerUrl}`);
2819
+ if (instance.port) log(`Port: ${instance.port}`);
2820
+ const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
2821
+ if (willBeHeadless) {
2822
+ const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
2823
+ log(`Headless: ${role}`);
2824
+ }
2825
+ try {
2826
+ const busyModeRaw = flags["busy-mode"];
2827
+ if (busyModeRaw !== void 0 && busyModeRaw !== "steer" && busyModeRaw !== "wait") {
2828
+ return {
2829
+ ok: false,
2830
+ command: "bridge",
2831
+ instanceId,
2832
+ runtime: instance.runtime,
2833
+ code: "TAP_INVALID_ARGUMENT",
2834
+ message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
2835
+ warnings: [],
2836
+ data: {}
2837
+ };
2838
+ }
2839
+ const busyMode = busyModeRaw;
2840
+ const pollSeconds = typeof flags["poll-seconds"] === "string" ? parseInt(flags["poll-seconds"], 10) : void 0;
2841
+ const reconnectSeconds = typeof flags["reconnect-seconds"] === "string" ? parseInt(flags["reconnect-seconds"], 10) : void 0;
2842
+ const messageLookbackMinutes = typeof flags["message-lookback-minutes"] === "string" ? parseInt(flags["message-lookback-minutes"], 10) : void 0;
2843
+ const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
2844
+ const ephemeral = flags["ephemeral"] === true;
2845
+ const processExistingMessages = flags["process-existing-messages"] === true;
2846
+ const headlessFlag = flags["headless"] === true;
2847
+ const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
2848
+ const validRoles = ["reviewer", "validator", "long-running"];
2849
+ if (roleArg && !validRoles.includes(roleArg)) {
2850
+ return {
2851
+ ok: false,
2852
+ command: "bridge",
2853
+ instanceId,
2854
+ runtime: instance.runtime,
2855
+ code: "TAP_INVALID_ARGUMENT",
2856
+ message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
2857
+ warnings: [],
2858
+ data: {}
2859
+ };
2860
+ }
2861
+ const headless = headlessFlag ? {
2862
+ enabled: true,
2863
+ role: roleArg ?? "reviewer",
2864
+ maxRounds: 5,
2865
+ qualitySeverityFloor: "high"
2866
+ } : instance.headless;
2867
+ const bridge = await startBridge({
2868
+ instanceId,
2869
+ runtime: instance.runtime,
2870
+ stateDir: ctx.stateDir,
2871
+ commsDir: ctx.commsDir,
2872
+ bridgeScript,
2873
+ platform: ctx.platform,
2874
+ agentName,
2875
+ runtimeCommand,
2876
+ appServerUrl,
2877
+ repoRoot,
2878
+ port: instance.port ?? void 0,
2879
+ headless,
2880
+ busyMode,
2881
+ pollSeconds,
2882
+ reconnectSeconds,
2883
+ messageLookbackMinutes,
2884
+ threadId,
2885
+ ephemeral,
2886
+ processExistingMessages
2887
+ });
2888
+ logSuccess(`Bridge started (PID: ${bridge.pid})`);
2889
+ log(`Log: ${path13.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
2890
+ const updated = { ...instance, bridge };
2891
+ const newState = updateInstanceState(state, instanceId, updated);
2892
+ saveState(repoRoot, newState);
2893
+ return {
2894
+ ok: true,
2895
+ command: "bridge",
2896
+ instanceId,
2897
+ runtime: instance.runtime,
2898
+ code: "TAP_BRIDGE_START_OK",
2899
+ message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
2900
+ warnings: [],
2901
+ data: { pid: bridge.pid }
2902
+ };
2903
+ } catch (err) {
2904
+ const msg = err instanceof Error ? err.message : String(err);
2905
+ logError(msg);
2906
+ return {
2907
+ ok: false,
2908
+ command: "bridge",
2909
+ instanceId,
2910
+ runtime: instance.runtime,
2911
+ code: "TAP_BRIDGE_START_FAILED",
2912
+ message: msg,
2913
+ warnings: [],
2914
+ data: {}
2915
+ };
2916
+ }
2917
+ }
2918
+ async function bridgeStopOne(identifier) {
2919
+ const repoRoot = findRepoRoot2();
2920
+ const state = loadState(repoRoot);
2921
+ if (!state) {
2922
+ return {
2923
+ ok: false,
2924
+ command: "bridge",
2925
+ code: "TAP_NOT_INITIALIZED",
2926
+ message: "Not initialized. Run: npx @hua-labs/tap init",
2927
+ warnings: [],
2928
+ data: {}
2929
+ };
2930
+ }
2931
+ const resolved = resolveInstanceId(identifier, state);
2932
+ if (!resolved.ok) {
2933
+ return {
2934
+ ok: false,
2935
+ command: "bridge",
2936
+ code: resolved.code,
2937
+ message: resolved.message,
2938
+ warnings: [],
2939
+ data: {}
2940
+ };
2941
+ }
2942
+ const instanceId = resolved.instanceId;
2943
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
2944
+ logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
2945
+ const stopped = await stopBridge({
2946
+ instanceId,
2947
+ stateDir: ctx.stateDir,
2948
+ platform: ctx.platform
2949
+ });
2950
+ 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
+ return {
2959
+ ok: true,
2960
+ command: "bridge",
2961
+ instanceId,
2962
+ code: "TAP_BRIDGE_STOP_OK",
2963
+ message: `Bridge for ${instanceId} stopped`,
2964
+ warnings: [],
2965
+ data: {}
2966
+ };
2967
+ }
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
+ return {
2976
+ ok: true,
2977
+ command: "bridge",
2978
+ instanceId,
2979
+ code: "TAP_BRIDGE_NOT_RUNNING",
2980
+ message: `No running bridge for ${instanceId}`,
2981
+ warnings: [],
2982
+ data: {}
2983
+ };
2984
+ }
2985
+ async function bridgeStopAll() {
2986
+ const repoRoot = findRepoRoot2();
2987
+ const state = loadState(repoRoot);
2988
+ if (!state) {
2989
+ return {
2990
+ ok: false,
2991
+ command: "bridge",
2992
+ code: "TAP_NOT_INITIALIZED",
2993
+ message: "Not initialized. Run: npx @hua-labs/tap init",
2994
+ warnings: [],
2995
+ data: {}
2996
+ };
2997
+ }
2998
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
2999
+ const instanceIds = Object.keys(state.instances);
3000
+ const stopped = [];
3001
+ logHeader("@hua-labs/tap bridge stop (all)");
3002
+ let stateChanged = false;
3003
+ for (const instanceId of instanceIds) {
3004
+ const didStop = await stopBridge({
3005
+ instanceId,
3006
+ stateDir: ctx.stateDir,
3007
+ platform: ctx.platform
3008
+ });
3009
+ if (didStop) {
3010
+ logSuccess(`Stopped bridge for ${instanceId}`);
3011
+ stopped.push(instanceId);
3012
+ }
3013
+ const instance = state.instances[instanceId];
3014
+ if (instance?.bridge) {
3015
+ state.instances[instanceId] = { ...instance, bridge: null };
3016
+ stateChanged = true;
3017
+ }
3018
+ }
3019
+ if (stateChanged) {
3020
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3021
+ saveState(repoRoot, state);
3022
+ }
3023
+ const message = stopped.length > 0 ? `Stopped ${stopped.length} bridge(s): ${stopped.join(", ")}` : "No running bridges found";
3024
+ log(message);
3025
+ return {
3026
+ ok: true,
3027
+ command: "bridge",
3028
+ code: stopped.length > 0 ? "TAP_BRIDGE_STOP_OK" : "TAP_BRIDGE_NOT_RUNNING",
3029
+ message,
3030
+ warnings: [],
3031
+ data: { stopped }
3032
+ };
3033
+ }
3034
+ function bridgeStatusAll() {
3035
+ const repoRoot = findRepoRoot2();
3036
+ const state = loadState(repoRoot);
3037
+ if (!state) {
3038
+ return {
3039
+ ok: false,
3040
+ command: "bridge",
3041
+ code: "TAP_NOT_INITIALIZED",
3042
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3043
+ warnings: [],
3044
+ data: {}
3045
+ };
3046
+ }
3047
+ const { config: resolvedCfg } = resolveConfig({}, repoRoot);
3048
+ const stateDir = resolvedCfg.stateDir;
3049
+ const instanceIds = Object.keys(state.instances);
3050
+ const bridges = {};
3051
+ logHeader("@hua-labs/tap bridge status");
3052
+ log(
3053
+ `${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(10)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Last Heartbeat"}`
3054
+ );
3055
+ log(
3056
+ `${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(10)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(20)}`
3057
+ );
3058
+ for (const instanceId of instanceIds) {
3059
+ const inst = state.instances[instanceId];
3060
+ if (!inst?.installed) continue;
3061
+ if (inst.bridgeMode !== "app-server") {
3062
+ log(
3063
+ `${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${"n/a".padEnd(10)} ${"-".padEnd(8)} ${"-".padEnd(6)} ${inst.bridgeMode} mode`
3064
+ );
3065
+ bridges[instanceId] = {
3066
+ status: "n/a",
3067
+ runtime: inst.runtime,
3068
+ pid: null,
3069
+ port: inst.port,
3070
+ lastHeartbeat: null
3071
+ };
3072
+ continue;
3073
+ }
3074
+ const status = getBridgeStatus(stateDir, instanceId);
3075
+ const bridgeState = loadBridgeState(stateDir, instanceId);
3076
+ const age = getHeartbeatAge(stateDir, instanceId);
3077
+ const pid = bridgeState?.pid ?? null;
3078
+ const heartbeat = bridgeState?.lastHeartbeat ?? null;
3079
+ const pidStr = pid ? String(pid) : "-";
3080
+ const portStr = inst.port ? String(inst.port) : "-";
3081
+ const ageStr = age !== null ? formatAge(age) : "-";
3082
+ const statusColor = status === "running" ? "running" : status === "stale" ? "stale!" : "stopped";
3083
+ log(
3084
+ `${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${statusColor.padEnd(10)} ${pidStr.padEnd(8)} ${portStr.padEnd(6)} ${ageStr}`
3085
+ );
3086
+ bridges[instanceId] = {
3087
+ status,
3088
+ runtime: inst.runtime,
3089
+ pid,
3090
+ port: inst.port,
3091
+ lastHeartbeat: heartbeat
3092
+ };
3093
+ }
3094
+ if (instanceIds.length === 0) {
3095
+ log("No instances installed.");
3096
+ }
3097
+ log("");
3098
+ return {
3099
+ ok: true,
3100
+ command: "bridge",
3101
+ code: "TAP_BRIDGE_STATUS_OK",
3102
+ message: `${instanceIds.length} instance(s) checked`,
3103
+ warnings: [],
3104
+ data: { bridges }
3105
+ };
3106
+ }
3107
+ function bridgeStatusOne(identifier) {
3108
+ const repoRoot = findRepoRoot2();
3109
+ const state = loadState(repoRoot);
3110
+ if (!state) {
3111
+ return {
3112
+ ok: false,
3113
+ command: "bridge",
3114
+ code: "TAP_NOT_INITIALIZED",
3115
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3116
+ warnings: [],
3117
+ data: {}
3118
+ };
3119
+ }
3120
+ const resolved = resolveInstanceId(identifier, state);
3121
+ if (!resolved.ok) {
3122
+ return {
3123
+ ok: false,
3124
+ command: "bridge",
3125
+ code: resolved.code,
3126
+ message: resolved.message,
3127
+ warnings: [],
3128
+ data: {}
3129
+ };
3130
+ }
3131
+ const instanceId = resolved.instanceId;
3132
+ const inst = state.instances[instanceId];
3133
+ if (!inst?.installed) {
3134
+ return {
3135
+ ok: false,
3136
+ command: "bridge",
3137
+ instanceId,
3138
+ code: "TAP_INSTANCE_NOT_FOUND",
3139
+ message: `${instanceId} is not installed.`,
3140
+ warnings: [],
3141
+ data: {}
3142
+ };
3143
+ }
3144
+ logHeader(`@hua-labs/tap bridge status ${instanceId}`);
3145
+ log(`Instance: ${instanceId}`);
3146
+ log(`Runtime: ${inst.runtime}`);
3147
+ log(`Bridge mode: ${inst.bridgeMode}`);
3148
+ if (inst.port) log(`Port: ${inst.port}`);
3149
+ if (inst.bridgeMode !== "app-server") {
3150
+ log(`Status: n/a (${inst.bridgeMode} mode)`);
3151
+ log("");
3152
+ return {
3153
+ ok: true,
3154
+ command: "bridge",
3155
+ instanceId,
3156
+ runtime: inst.runtime,
3157
+ code: "TAP_BRIDGE_STATUS_OK",
3158
+ message: `${instanceId} bridge: n/a (${inst.bridgeMode} mode)`,
3159
+ warnings: [],
3160
+ data: {
3161
+ status: "n/a",
3162
+ bridgeMode: inst.bridgeMode,
3163
+ pid: null,
3164
+ port: inst.port,
3165
+ lastHeartbeat: null
3166
+ }
3167
+ };
3168
+ }
3169
+ const { config: resolvedCfg2 } = resolveConfig({}, repoRoot);
3170
+ const stateDir = resolvedCfg2.stateDir;
3171
+ const status = getBridgeStatus(stateDir, instanceId);
3172
+ const bridgeState = loadBridgeState(stateDir, instanceId);
3173
+ const age = getHeartbeatAge(stateDir, instanceId);
3174
+ log(`Status: ${status}`);
3175
+ if (bridgeState) {
3176
+ log(`PID: ${bridgeState.pid}`);
3177
+ log(
3178
+ `Heartbeat: ${bridgeState.lastHeartbeat}${age !== null ? ` (${formatAge(age)})` : ""}`
3179
+ );
3180
+ log(
3181
+ `Log: ${path13.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
3182
+ );
3183
+ }
3184
+ log("");
3185
+ return {
3186
+ ok: true,
3187
+ command: "bridge",
3188
+ instanceId,
3189
+ runtime: inst.runtime,
3190
+ code: "TAP_BRIDGE_STATUS_OK",
3191
+ message: `${instanceId} bridge: ${status}`,
3192
+ warnings: [],
3193
+ data: {
3194
+ status,
3195
+ bridgeMode: inst.bridgeMode,
3196
+ pid: bridgeState?.pid ?? null,
3197
+ port: inst.port,
3198
+ lastHeartbeat: bridgeState?.lastHeartbeat ?? null
3199
+ }
3200
+ };
3201
+ }
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);
3209
+ return {
3210
+ ok: true,
3211
+ command: "bridge",
3212
+ code: "TAP_NO_OP",
3213
+ message: BRIDGE_HELP,
3214
+ warnings: [],
3215
+ data: {}
3216
+ };
3217
+ }
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);
3241
+ }
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
+ };
3253
+ }
3254
+ }
3255
+
3256
+ // 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
+ }
3285
+ async function serveCommand(args) {
3286
+ const repoRoot = findRepoRoot2();
3287
+ let commsDir;
3288
+ const commsDirIdx = args.indexOf("--comms-dir");
3289
+ if (commsDirIdx !== -1 && args[commsDirIdx + 1]) {
3290
+ commsDir = path14.resolve(args[commsDirIdx + 1]);
3291
+ }
3292
+ if (!commsDir && process.env.TAP_COMMS_DIR) {
3293
+ commsDir = process.env.TAP_COMMS_DIR;
3294
+ }
3295
+ if (!commsDir) {
3296
+ const state = loadState(repoRoot);
3297
+ if (state) {
3298
+ commsDir = state.commsDir;
3299
+ }
3300
+ }
3301
+ if (!commsDir) {
3302
+ return {
3303
+ ok: false,
3304
+ command: "serve",
3305
+ code: "TAP_NOT_INITIALIZED",
3306
+ message: "Cannot determine comms directory. Set TAP_COMMS_DIR env var, use --comms-dir, or run 'init' first.",
3307
+ warnings: [],
3308
+ data: {}
3309
+ };
3310
+ }
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) {
3323
+ return {
3324
+ ok: false,
3325
+ 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/.",
3328
+ warnings: [],
3329
+ data: {}
3330
+ };
3331
+ }
3332
+ const child = spawn2("bun", [serverEntry], {
3333
+ stdio: "inherit",
3334
+ env: {
3335
+ ...process.env,
3336
+ TAP_COMMS_DIR: commsDir
3337
+ }
3338
+ });
3339
+ return new Promise((resolve10) => {
3340
+ child.on("error", (err) => {
3341
+ resolve10({
3342
+ ok: false,
3343
+ command: "serve",
3344
+ code: "TAP_INTERNAL_ERROR",
3345
+ message: `Failed to start MCP server: ${err.message}`,
3346
+ warnings: [],
3347
+ data: {}
3348
+ });
3349
+ });
3350
+ child.on("exit", (code) => {
3351
+ resolve10({
3352
+ ok: code === 0,
3353
+ command: "serve",
3354
+ code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
3355
+ message: code === 0 ? "MCP server stopped" : `MCP server exited with code ${code}`,
3356
+ warnings: [],
3357
+ data: { exitCode: code }
3358
+ });
3359
+ });
3360
+ });
3361
+ }
3362
+
3363
+ // 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";
3367
+ var INIT_WORKTREE_HELP = `
3368
+ Usage:
3369
+ tap-comms init-worktree [options]
3370
+
3371
+ Options:
3372
+ --path <dir> Worktree directory (required, e.g. ../hua-wt-3)
3373
+ --branch <name> Branch name to create (default: derived from path)
3374
+ --base <ref> Base ref for new branch (default: origin/main)
3375
+ --mission <file> Mission file to associate (e.g. m74-feature.md)
3376
+ --comms-dir <path> Override comms directory
3377
+ --skip-install Skip pnpm install step
3378
+ --help, -h Show help
3379
+
3380
+ Examples:
3381
+ npx @hua-labs/tap init-worktree --path ../hua-wt-3 --branch feat/my-feature
3382
+ npx @hua-labs/tap init-worktree --path ../hua-wt-4 --branch fix/bug --mission m74-fix.md
3383
+ `.trim();
3384
+ function warn(warnings, message) {
3385
+ logWarn(message);
3386
+ warnings.push(message);
3387
+ }
3388
+ function run(cmd, opts) {
3389
+ try {
3390
+ return execSync5(cmd, {
3391
+ cwd: opts?.cwd,
3392
+ encoding: "utf-8",
3393
+ stdio: ["pipe", "pipe", "pipe"],
3394
+ timeout: 12e4
3395
+ }).trim();
3396
+ } catch (err) {
3397
+ if (opts?.ignoreError) return "";
3398
+ throw err;
3399
+ }
3400
+ }
3401
+ function toAbsolute(p) {
3402
+ const resolved = path15.resolve(p);
3403
+ return resolved.replace(/\\/g, "/");
3404
+ }
3405
+ function probeBun(candidate) {
3406
+ try {
3407
+ const out = execSync5(`"${candidate}" --version`, {
3408
+ encoding: "utf-8",
3409
+ stdio: ["pipe", "pipe", "pipe"],
3410
+ timeout: 5e3
3411
+ }).trim();
3412
+ return /^\d+\.\d+/.test(out);
3413
+ } catch {
3414
+ return false;
3415
+ }
3416
+ }
3417
+ function findBun() {
3418
+ const candidates = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
3419
+ for (const name of candidates) {
3420
+ try {
3421
+ const out = execSync5(
3422
+ process.platform === "win32" ? `where ${name}` : `which ${name}`,
3423
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
3424
+ ).trim();
3425
+ for (const line of out.split(/\r?\n/)) {
3426
+ const trimmed = line.trim();
3427
+ if (trimmed && probeBun(trimmed)) return trimmed;
3428
+ }
3429
+ } catch {
3430
+ }
3431
+ }
3432
+ const home = process.env.HOME || process.env.USERPROFILE || "";
3433
+ const bunHome = path15.join(
3434
+ home,
3435
+ ".bun",
3436
+ "bin",
3437
+ process.platform === "win32" ? "bun.exe" : "bun"
3438
+ );
3439
+ if (fs15.existsSync(bunHome) && probeBun(bunHome)) return bunHome;
3440
+ return null;
3441
+ }
3442
+ function step1CreateWorktree(opts) {
3443
+ log("Step 1/9: Creating worktree...");
3444
+ if (fs15.existsSync(opts.worktreePath)) {
3445
+ logWarn(`Directory already exists: ${opts.worktreePath}`);
3446
+ try {
3447
+ run("git rev-parse --git-dir", { cwd: opts.worktreePath });
3448
+ logWarn("Already a git worktree. Continuing...");
3449
+ return true;
3450
+ } catch {
3451
+ logError("Directory exists but is not a git worktree.");
3452
+ return false;
3453
+ }
3454
+ }
3455
+ try {
3456
+ run(
3457
+ `git worktree add "${opts.worktreePath}" -b ${opts.branch} ${opts.base}`,
3458
+ { cwd: opts.repoRoot }
3459
+ );
3460
+ logSuccess(`Worktree created: ${opts.worktreePath}`);
3461
+ logSuccess(`Branch: ${opts.branch} (from ${opts.base})`);
3462
+ } catch {
3463
+ try {
3464
+ run(`git worktree add "${opts.worktreePath}" ${opts.branch}`, {
3465
+ cwd: opts.repoRoot
3466
+ });
3467
+ logSuccess(`Worktree created with existing branch: ${opts.branch}`);
3468
+ } catch (err) {
3469
+ logError(
3470
+ `Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`
3471
+ );
3472
+ return false;
3473
+ }
3474
+ }
3475
+ return true;
3476
+ }
3477
+ function step2MergeMain(opts, warnings) {
3478
+ log("Step 2/9: Merging origin/main...");
3479
+ try {
3480
+ run("git fetch origin main", { cwd: opts.worktreePath });
3481
+ } catch {
3482
+ warn(warnings, "Could not fetch origin/main. Skipping merge.");
3483
+ return;
3484
+ }
3485
+ try {
3486
+ const behind = run("git rev-list --count HEAD..origin/main", {
3487
+ cwd: opts.worktreePath
3488
+ });
3489
+ if (behind === "0") {
3490
+ logSuccess("Already up to date with origin/main.");
3491
+ return;
3492
+ }
3493
+ run("git merge origin/main --no-edit -X theirs", {
3494
+ cwd: opts.worktreePath
3495
+ });
3496
+ logSuccess("Merged origin/main.");
3497
+ } catch {
3498
+ warn(
3499
+ warnings,
3500
+ "Merge had issues. You may need to resolve conflicts manually."
3501
+ );
3502
+ }
3503
+ }
3504
+ function step3CopyPermissions(opts, warnings) {
3505
+ log("Step 3/9: Copying permissions...");
3506
+ const srcSettings = path15.join(
3507
+ opts.repoRoot,
3508
+ ".claude",
3509
+ "settings.local.json"
3510
+ );
3511
+ const destDir = path15.join(opts.worktreePath, ".claude");
3512
+ const destSettings = path15.join(destDir, "settings.local.json");
3513
+ if (!fs15.existsSync(srcSettings)) {
3514
+ warn(
3515
+ warnings,
3516
+ "No .claude/settings.local.json found in main repo. Skipping."
3517
+ );
3518
+ return;
3519
+ }
3520
+ fs15.mkdirSync(destDir, { recursive: true });
3521
+ fs15.copyFileSync(srcSettings, destSettings);
3522
+ logSuccess("Copied settings.local.json");
3523
+ try {
3524
+ run("git update-index --skip-worktree .claude/settings.local.json", {
3525
+ cwd: opts.worktreePath
3526
+ });
3527
+ logSuccess("Marked skip-worktree");
3528
+ } catch {
3529
+ warn(warnings, "Could not set skip-worktree. File may show as modified.");
3530
+ }
3531
+ }
3532
+ function step4GenerateMcpJson(opts, warnings) {
3533
+ log("Step 4/9: Generating .mcp.json...");
3534
+ const bunPath = findBun();
3535
+ if (!bunPath) {
3536
+ warn(warnings, "bun not found. .mcp.json not generated.");
3537
+ warn(
3538
+ warnings,
3539
+ "Install bun (https://bun.sh) and re-run, or create .mcp.json manually."
3540
+ );
3541
+ return;
3542
+ }
3543
+ const wtAbs = toAbsolute(opts.worktreePath);
3544
+ const bunAbs = toAbsolute(bunPath);
3545
+ const commsAbs = toAbsolute(opts.commsDir);
3546
+ const channelEntry = path15.join(
3547
+ wtAbs,
3548
+ "packages/tap-plugin/channels/tap-comms.ts"
3549
+ );
3550
+ const mcpConfig = {
3551
+ mcpServers: {
3552
+ "tap-comms": {
3553
+ command: bunAbs,
3554
+ args: [channelEntry],
3555
+ cwd: wtAbs,
3556
+ env: {
3557
+ TAP_COMMS_DIR: commsAbs,
3558
+ TAP_AGENT_NAME: "unnamed"
3559
+ }
3560
+ }
3561
+ }
3562
+ };
3563
+ const mcpPath = path15.join(opts.worktreePath, ".mcp.json");
3564
+ fs15.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
3565
+ logSuccess(`.mcp.json generated (absolute paths + cwd)`);
3566
+ log(` bun: ${bunAbs}`);
3567
+ log(` comms: ${commsAbs}`);
3568
+ }
3569
+ function step5Install(opts, warnings) {
3570
+ if (opts.skipInstall) {
3571
+ log("Step 5/9: Skipping pnpm install (--skip-install).");
3572
+ return;
3573
+ }
3574
+ log("Step 5/9: Installing dependencies...");
3575
+ try {
3576
+ run("pnpm install --prefer-offline", { cwd: opts.worktreePath });
3577
+ logSuccess("Dependencies installed.");
3578
+ } catch {
3579
+ warn(
3580
+ warnings,
3581
+ "pnpm install failed. Try running manually in the worktree."
3582
+ );
3583
+ }
3584
+ }
3585
+ function step6BuildEslintPlugin(opts, warnings) {
3586
+ if (opts.skipInstall) {
3587
+ log("Step 6/9: Skipping eslint plugin build (--skip-install).");
3588
+ return;
3589
+ }
3590
+ log("Step 6/9: Building eslint-plugin-i18n...");
3591
+ try {
3592
+ run("pnpm build --filter @hua-labs/eslint-plugin-i18n", {
3593
+ cwd: opts.worktreePath
3594
+ });
3595
+ logSuccess("eslint-plugin-i18n built.");
3596
+ } catch {
3597
+ warn(warnings, "eslint-plugin-i18n build failed. Non-blocking.");
3598
+ }
3599
+ }
3600
+ function step7VerifyComms(opts, warnings) {
3601
+ log("Step 7/9: Verifying comms directory...");
3602
+ if (!fs15.existsSync(opts.commsDir)) {
3603
+ warn(warnings, `Comms directory not found: ${opts.commsDir}`);
3604
+ warn(warnings, "Create it or run: npx @hua-labs/tap init");
3605
+ return;
3606
+ }
3607
+ const requiredDirs = ["inbox", "findings", "reviews", "letters"];
3608
+ for (const dir of requiredDirs) {
3609
+ const dirPath = path15.join(opts.commsDir, dir);
3610
+ if (!fs15.existsSync(dirPath)) {
3611
+ fs15.mkdirSync(dirPath, { recursive: true });
3612
+ logSuccess(`Created ${dir}/`);
3613
+ }
3614
+ }
3615
+ logSuccess(`Comms verified: ${opts.commsDir}`);
3616
+ }
3617
+ function step8VerifyBun(warnings) {
3618
+ log("Step 8/9: Verifying bun...");
3619
+ const bunPath = findBun();
3620
+ if (!bunPath) {
3621
+ warn(warnings, "bun not found in PATH.");
3622
+ warn(warnings, "Install: curl -fsSL https://bun.sh/install | bash");
3623
+ return;
3624
+ }
3625
+ try {
3626
+ const version2 = run(`"${bunPath}" --version`);
3627
+ logSuccess(`bun ${version2} found: ${bunPath}`);
3628
+ } catch {
3629
+ warn(warnings, "bun found but version check failed.");
3630
+ }
3631
+ }
3632
+ function step9Ready(opts) {
3633
+ logHeader("Ready!");
3634
+ log(`Worktree: ${toAbsolute(opts.worktreePath)}`);
3635
+ log(`Branch: ${opts.branch}`);
3636
+ log(`Comms: ${toAbsolute(opts.commsDir)}`);
3637
+ if (opts.mission) log(`Mission: ${opts.mission}`);
3638
+ log("");
3639
+ log("Next steps:");
3640
+ log(` cd ${opts.worktreePath}`);
3641
+ log(" claude # Start Claude Code session");
3642
+ log("");
3643
+ }
3644
+ async function initWorktreeCommand(args) {
3645
+ const { flags } = parseArgs(args);
3646
+ if (flags["help"] === true || flags["h"] === true) {
3647
+ log(INIT_WORKTREE_HELP);
3648
+ return {
3649
+ ok: true,
3650
+ command: "init-worktree",
3651
+ code: "TAP_NO_OP",
3652
+ message: "init-worktree help",
3653
+ warnings: [],
3654
+ data: {}
3655
+ };
3656
+ }
3657
+ const worktreePath = typeof flags["path"] === "string" ? flags["path"] : void 0;
3658
+ if (!worktreePath) {
3659
+ return {
3660
+ ok: false,
3661
+ command: "init-worktree",
3662
+ code: "TAP_INVALID_ARGUMENT",
3663
+ message: "Missing --path. Usage: npx @hua-labs/tap init-worktree --path ../hua-wt-3",
3664
+ warnings: [],
3665
+ data: {}
3666
+ };
3667
+ }
3668
+ const repoRoot = findRepoRoot2();
3669
+ const { config } = resolveConfig({}, repoRoot);
3670
+ const branch = typeof flags["branch"] === "string" ? flags["branch"] : path15.basename(path15.resolve(worktreePath));
3671
+ const base = typeof flags["base"] === "string" ? flags["base"] : "origin/main";
3672
+ const mission = typeof flags["mission"] === "string" ? flags["mission"] : void 0;
3673
+ const commsDir = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : config.commsDir;
3674
+ const skipInstall = flags["skip-install"] === true;
3675
+ const opts = {
3676
+ worktreePath: path15.resolve(worktreePath),
3677
+ branch,
3678
+ base,
3679
+ mission,
3680
+ commsDir: path15.resolve(commsDir),
3681
+ skipInstall,
3682
+ repoRoot
3683
+ };
3684
+ logHeader(`@hua-labs/tap init-worktree`);
3685
+ log(`Path: ${opts.worktreePath}`);
3686
+ log(`Branch: ${opts.branch}`);
3687
+ log(`Base: ${opts.base}`);
3688
+ log(`Comms: ${opts.commsDir}`);
3689
+ if (mission) log(`Mission: ${mission}`);
3690
+ log("");
3691
+ const warnings = [];
3692
+ const created = step1CreateWorktree(opts);
3693
+ if (!created) {
3694
+ return {
3695
+ ok: false,
3696
+ command: "init-worktree",
3697
+ code: "TAP_PATCH_FAILED",
3698
+ message: "Failed to create worktree.",
3699
+ warnings,
3700
+ data: {}
3701
+ };
3702
+ }
3703
+ step2MergeMain(opts, warnings);
3704
+ step3CopyPermissions(opts, warnings);
3705
+ step4GenerateMcpJson(opts, warnings);
3706
+ step5Install(opts, warnings);
3707
+ step6BuildEslintPlugin(opts, warnings);
3708
+ step7VerifyComms(opts, warnings);
3709
+ step8VerifyBun(warnings);
3710
+ step9Ready(opts);
3711
+ return {
3712
+ ok: true,
3713
+ command: "init-worktree",
3714
+ code: "TAP_INIT_OK",
3715
+ message: `Worktree initialized: ${opts.worktreePath}`,
3716
+ warnings,
3717
+ data: {
3718
+ path: opts.worktreePath,
3719
+ branch: opts.branch,
3720
+ commsDir: opts.commsDir
3721
+ }
3722
+ };
3723
+ }
3724
+
3725
+ // src/engine/dashboard.ts
3726
+ import * as fs16 from "fs";
3727
+ import * as path16 from "path";
3728
+ import { execSync as execSync6 } from "child_process";
3729
+ function collectAgents(commsDir) {
3730
+ const heartbeatsPath = path16.join(commsDir, "heartbeats.json");
3731
+ if (!fs16.existsSync(heartbeatsPath)) return [];
3732
+ try {
3733
+ const raw = fs16.readFileSync(heartbeatsPath, "utf-8");
3734
+ const data = JSON.parse(raw);
3735
+ return Object.entries(data).map(([name, info]) => ({
3736
+ name: info.agent ?? name,
3737
+ status: info.status ?? null,
3738
+ lastActivity: info.lastActivity ?? info.timestamp ?? null,
3739
+ joinedAt: info.joinedAt ?? null
3740
+ }));
3741
+ } catch {
3742
+ return [];
3743
+ }
3744
+ }
3745
+ function collectBridges(repoRoot) {
3746
+ const state = loadState(repoRoot);
3747
+ const { config } = resolveConfig({}, repoRoot);
3748
+ const stateDir = config.stateDir;
3749
+ const bridges = [];
3750
+ if (state) {
3751
+ for (const [id, inst] of Object.entries(state.instances)) {
3752
+ if (!inst?.installed) continue;
3753
+ if (inst.bridgeMode !== "app-server") continue;
3754
+ const instanceId = id;
3755
+ const status = getBridgeStatus(stateDir, instanceId);
3756
+ const bridgeState = loadBridgeState(stateDir, instanceId);
3757
+ const age = getHeartbeatAge(stateDir, instanceId);
3758
+ bridges.push({
3759
+ instanceId: id,
3760
+ runtime: inst.runtime,
3761
+ status,
3762
+ pid: bridgeState?.pid ?? null,
3763
+ port: inst.port ?? null,
3764
+ heartbeatAge: age,
3765
+ headless: inst.headless?.enabled ?? false
3766
+ });
3767
+ }
3768
+ }
3769
+ const tmpDir = path16.join(repoRoot, ".tmp");
3770
+ if (fs16.existsSync(tmpDir)) {
3771
+ try {
3772
+ const dirs = fs16.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
3773
+ for (const dir of dirs) {
3774
+ const daemonPath = path16.join(tmpDir, dir, "bridge-daemon.json");
3775
+ if (!fs16.existsSync(daemonPath)) continue;
3776
+ try {
3777
+ const raw = fs16.readFileSync(daemonPath, "utf-8");
3778
+ const daemon = JSON.parse(raw);
3779
+ const alreadyCovered = bridges.some(
3780
+ (b) => b.pid === daemon.pid && b.pid !== null
3781
+ );
3782
+ if (alreadyCovered) continue;
3783
+ const agentFile = path16.join(tmpDir, dir, "agent-name.txt");
3784
+ const agentName = fs16.existsSync(agentFile) ? fs16.readFileSync(agentFile, "utf-8").trim() : dir;
3785
+ const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
3786
+ const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
3787
+ const port = portMatch ? parseInt(portMatch[1], 10) : null;
3788
+ bridges.push({
3789
+ instanceId: agentName,
3790
+ runtime: "codex",
3791
+ status: running ? "running" : "stale",
3792
+ pid: daemon.pid ?? null,
3793
+ port,
3794
+ heartbeatAge: null,
3795
+ headless: false
3796
+ });
3797
+ } catch {
3798
+ }
3799
+ }
3800
+ } catch {
3801
+ }
3802
+ }
3803
+ return bridges;
3804
+ }
3805
+ function collectPRs() {
3806
+ try {
3807
+ const output = execSync6(
3808
+ "gh pr list --state all --limit 10 --json number,title,author,state,url",
3809
+ { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
3810
+ );
3811
+ const prs = JSON.parse(output);
3812
+ return prs.map((pr) => ({
3813
+ number: pr.number,
3814
+ title: pr.title,
3815
+ author: pr.author.login,
3816
+ state: pr.state,
3817
+ url: pr.url
3818
+ }));
3819
+ } catch {
3820
+ return [];
3821
+ }
3822
+ }
3823
+ function collectWarnings(bridges, agents) {
3824
+ const warnings = [];
3825
+ for (const bridge of bridges) {
3826
+ if (bridge.status === "stale") {
3827
+ warnings.push({
3828
+ level: "warn",
3829
+ message: `Bridge ${bridge.instanceId} is stale (PID ${bridge.pid} dead)`
3830
+ });
3831
+ }
3832
+ if (bridge.status === "running" && bridge.heartbeatAge !== null && bridge.heartbeatAge > 60) {
3833
+ warnings.push({
3834
+ level: "warn",
3835
+ message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
3836
+ });
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
+ });
3850
+ }
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);
3863
+ return {
3864
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3865
+ repoRoot: resolved.repoRoot,
3866
+ commsDir: resolved.commsDir,
3867
+ agents,
3868
+ bridges,
3869
+ prs,
3870
+ warnings
3871
+ };
3872
+ }
3873
+
3874
+ // src/commands/dashboard.ts
3875
+ function formatAge2(seconds) {
3876
+ if (seconds === null) return "-";
3877
+ if (seconds < 60) return `${seconds}s ago`;
3878
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
3879
+ return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
3880
+ }
3881
+ function formatStatus(status) {
3882
+ switch (status) {
3883
+ case "running":
3884
+ return "running";
3885
+ case "stale":
3886
+ return "stale!";
3887
+ case "stopped":
3888
+ return "stopped";
3889
+ case "MERGED":
3890
+ return "merged";
3891
+ case "OPEN":
3892
+ return "open";
3893
+ case "CLOSED":
3894
+ return "closed";
3895
+ default:
3896
+ return status;
3897
+ }
3898
+ }
3899
+ function truncate(str, len) {
3900
+ return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
3901
+ }
3902
+ function renderSnapshot(snapshot) {
3903
+ logHeader("tap dashboard");
3904
+ log(`Time: ${snapshot.generatedAt}`);
3905
+ log(`Repo: ${snapshot.repoRoot}`);
3906
+ log(`Comms: ${snapshot.commsDir}`);
3907
+ log("");
3908
+ log("\u2500\u2500 Agents \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3909
+ if (snapshot.agents.length === 0) {
3910
+ log(" (no heartbeats)");
3911
+ } else {
3912
+ for (const agent of snapshot.agents) {
3913
+ const activity = agent.lastActivity ? formatAge2(
3914
+ Math.floor(
3915
+ (Date.now() - new Date(agent.lastActivity).getTime()) / 1e3
3916
+ )
3917
+ ) : "unknown";
3918
+ const status = agent.status ?? "unknown";
3919
+ log(` ${agent.name.padEnd(12)} ${status.padEnd(10)} active ${activity}`);
3920
+ }
3921
+ }
3922
+ log("");
3923
+ log("\u2500\u2500 Bridges \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3924
+ if (snapshot.bridges.length === 0) {
3925
+ log(" (none)");
3926
+ } else {
3927
+ log(
3928
+ ` ${"Instance".padEnd(20)} ${"Status".padEnd(10)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Heartbeat"}`
3929
+ );
3930
+ log(
3931
+ ` ${"\u2500".repeat(20)} ${"\u2500".repeat(10)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(12)}`
3932
+ );
3933
+ for (const b of snapshot.bridges) {
3934
+ const headlessTag = b.headless ? " [H]" : "";
3935
+ log(
3936
+ ` ${truncate(b.instanceId + headlessTag, 20).padEnd(20)} ${formatStatus(b.status).padEnd(10)} ${(b.pid ? String(b.pid) : "-").padEnd(8)} ${(b.port ? String(b.port) : "-").padEnd(6)} ${formatAge2(b.heartbeatAge)}`
3937
+ );
3938
+ }
3939
+ }
3940
+ log("");
3941
+ log("\u2500\u2500 PRs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3942
+ if (snapshot.prs.length === 0) {
3943
+ log(" (gh CLI unavailable or no PRs)");
3944
+ } else {
3945
+ for (const pr of snapshot.prs) {
3946
+ const icon = pr.state === "MERGED" ? "+" : pr.state === "OPEN" ? "~" : "x";
3947
+ log(
3948
+ ` ${icon} #${String(pr.number).padEnd(5)} ${formatStatus(pr.state).padEnd(8)} ${truncate(pr.title, 50)}`
3949
+ );
3950
+ }
3951
+ }
3952
+ log("");
3953
+ log("\u2500\u2500 Warnings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3954
+ if (snapshot.warnings.length === 0) {
3955
+ log(" [OK] no warnings");
3956
+ } else {
3957
+ for (const w of snapshot.warnings) {
3958
+ const prefix = w.level === "error" ? "[ERR]" : "[WARN]";
3959
+ log(` ${prefix} ${w.message}`);
3960
+ }
3961
+ }
3962
+ }
3963
+ async function dashboardCommand(args) {
3964
+ const { flags } = parseArgs(args);
3965
+ const jsonMode = flags["json"] === true;
3966
+ const watchMode = flags["watch"] === true;
3967
+ const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : "5";
3968
+ const intervalSeconds = Math.max(2, parseInt(intervalStr, 10) || 5);
3969
+ const commsDirOverride = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : void 0;
3970
+ const repoRoot = findRepoRoot2();
3971
+ if (watchMode) {
3972
+ const run2 = () => {
3973
+ const snapshot2 = collectDashboardSnapshot(repoRoot, commsDirOverride);
3974
+ if (jsonMode) {
3975
+ console.log(JSON.stringify(snapshot2, null, 2));
3976
+ } else {
3977
+ process.stdout.write("\x1B[2J\x1B[H");
3978
+ renderSnapshot(snapshot2);
3979
+ log("");
3980
+ log(` Refreshing every ${intervalSeconds}s \u2014 Ctrl+C to exit`);
3981
+ }
3982
+ };
3983
+ run2();
3984
+ const timer = setInterval(run2, intervalSeconds * 1e3);
3985
+ const cleanup = () => {
3986
+ clearInterval(timer);
3987
+ process.exit(0);
3988
+ };
3989
+ process.on("SIGINT", cleanup);
3990
+ process.on("SIGTERM", cleanup);
3991
+ await new Promise(() => {
3992
+ });
3993
+ return {
3994
+ ok: true,
3995
+ command: "unknown",
3996
+ code: "TAP_NO_OP",
3997
+ message: "Watch mode ended",
3998
+ warnings: [],
3999
+ data: {}
4000
+ };
4001
+ }
4002
+ const snapshot = collectDashboardSnapshot(repoRoot, commsDirOverride);
4003
+ if (jsonMode) {
4004
+ console.log(JSON.stringify(snapshot, null, 2));
4005
+ } else {
4006
+ renderSnapshot(snapshot);
4007
+ }
4008
+ return {
4009
+ ok: true,
4010
+ command: "dashboard",
4011
+ code: "TAP_STATUS_OK",
4012
+ message: `Dashboard: ${snapshot.bridges.length} bridge(s), ${snapshot.agents.length} agent(s), ${snapshot.prs.length} PR(s)`,
4013
+ warnings: snapshot.warnings.map((w) => w.message),
4014
+ data: snapshot
4015
+ };
4016
+ }
4017
+
4018
+ // src/output.ts
4019
+ function emitResult(result, jsonMode) {
4020
+ if (jsonMode) {
4021
+ console.log(JSON.stringify(result, null, 2));
4022
+ return;
4023
+ }
4024
+ if (result.ok) {
4025
+ logSuccess(result.message);
4026
+ } else {
4027
+ logError(result.message);
4028
+ }
4029
+ for (const w of result.warnings) {
4030
+ logWarn(w);
4031
+ }
4032
+ }
4033
+ function exitCode(result) {
4034
+ return result.ok ? 0 : 1;
4035
+ }
4036
+ function extractJsonFlag(args) {
4037
+ const jsonMode = args.includes("--json");
4038
+ const cleanArgs = args.filter((a) => a !== "--json");
4039
+ return { jsonMode, cleanArgs };
4040
+ }
4041
+
4042
+ // src/cli.ts
4043
+ var HELP = `
4044
+ @hua-labs/tap \u2014 Cross-model AI agent communication setup
4045
+
4046
+ Usage:
4047
+ tap-comms <command> [options]
4048
+
4049
+ Commands:
4050
+ init Initialize comms directory and state
4051
+ init-worktree Set up a new git worktree with tap-comms
4052
+ add <runtime> Add a runtime instance (claude, codex, gemini)
4053
+ remove <instance> Remove an instance and rollback config
4054
+ status Show installed instances and bridge status
4055
+ bridge <sub> [inst] Manage bridges (start, stop, status)
4056
+ dashboard Show unified ops dashboard
4057
+ serve Start tap-comms MCP server (stdio)
4058
+ version Show version
4059
+
4060
+ Options:
4061
+ --help, -h Show help
4062
+ --json Machine-readable JSON output
4063
+ --comms-dir <path> Override comms directory path
4064
+
4065
+ Examples:
4066
+ npx @hua-labs/tap init
4067
+ npx @hua-labs/tap init-worktree --path ../hua-wt-3 --branch feat/my-feature
4068
+ npx @hua-labs/tap add claude
4069
+ npx @hua-labs/tap add codex --name reviewer --port 4501
4070
+ npx @hua-labs/tap status
4071
+ `.trim();
4072
+ function normalizeCommandName(command) {
4073
+ switch (command) {
4074
+ case "init":
4075
+ case "init-worktree":
4076
+ case "add":
4077
+ case "remove":
4078
+ case "status":
4079
+ case "bridge":
4080
+ case "dashboard":
4081
+ case "serve":
4082
+ return command;
4083
+ default:
4084
+ return "unknown";
4085
+ }
4086
+ }
4087
+ async function main() {
4088
+ const rawArgs = process.argv.slice(2);
4089
+ const { jsonMode, cleanArgs } = extractJsonFlag(rawArgs);
4090
+ setJsonMode(jsonMode);
4091
+ const command = cleanArgs[0];
4092
+ if (!command || command === "--help" || command === "-h") {
4093
+ if (jsonMode) {
4094
+ console.log(JSON.stringify({ help: HELP }));
4095
+ } else {
4096
+ console.log(HELP);
4097
+ }
4098
+ process.exit(0);
4099
+ }
4100
+ if (command === "version" || command === "--version" || command === "-v") {
4101
+ if (jsonMode) {
4102
+ console.log(JSON.stringify({ version }));
4103
+ } else {
4104
+ console.log(`@hua-labs/tap v${version}`);
4105
+ }
4106
+ process.exit(0);
4107
+ }
4108
+ const commandArgs = cleanArgs.slice(1);
4109
+ let result;
4110
+ try {
4111
+ switch (command) {
4112
+ case "init":
4113
+ result = await initCommand(commandArgs);
4114
+ break;
4115
+ case "init-worktree":
4116
+ result = await initWorktreeCommand(commandArgs);
4117
+ break;
4118
+ case "add":
4119
+ result = await addCommand(commandArgs);
4120
+ break;
4121
+ case "remove":
4122
+ result = await removeCommand(commandArgs);
4123
+ break;
4124
+ case "status":
4125
+ result = await statusCommand(commandArgs);
4126
+ break;
4127
+ case "bridge":
4128
+ result = await bridgeCommand(commandArgs);
4129
+ break;
4130
+ case "dashboard":
4131
+ result = await dashboardCommand(commandArgs);
4132
+ break;
4133
+ case "serve": {
4134
+ const serveResult = await serveCommand(commandArgs);
4135
+ if (!serveResult.ok) {
4136
+ emitResult(serveResult, jsonMode);
4137
+ }
4138
+ process.exit(exitCode(serveResult));
4139
+ break;
4140
+ }
4141
+ default:
4142
+ result = {
4143
+ ok: false,
4144
+ command: "unknown",
4145
+ code: "TAP_INVALID_ARGUMENT",
4146
+ message: `Unknown command: ${command}`,
4147
+ warnings: [],
4148
+ data: { requestedCommand: command }
4149
+ };
4150
+ }
4151
+ } catch (err) {
4152
+ const message = err instanceof Error ? err.message : String(err);
4153
+ result = {
4154
+ ok: false,
4155
+ command: normalizeCommandName(command),
4156
+ code: "TAP_INTERNAL_ERROR",
4157
+ message,
4158
+ warnings: [],
4159
+ data: command ? { requestedCommand: command } : {}
4160
+ };
4161
+ }
4162
+ emitResult(result, jsonMode);
4163
+ process.exit(exitCode(result));
4164
+ }
4165
+ main();
4166
+ //# sourceMappingURL=cli.mjs.map