@hua-labs/tap 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/commands/init.ts
2
2
  import * as fs6 from "fs";
3
3
  import * as path6 from "path";
4
+ import { execSync } from "child_process";
4
5
 
5
6
  // src/state.ts
6
7
  import * as fs3 from "fs";
@@ -186,7 +187,7 @@ function loadJsonFile(filePath) {
186
187
  return null;
187
188
  }
188
189
  }
189
- function loadSharedConfig(repoRoot) {
190
+ function loadSharedConfig2(repoRoot) {
190
191
  return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
191
192
  }
192
193
  function loadLocalConfig(repoRoot) {
@@ -210,7 +211,7 @@ function loadLegacyShellConfig(repoRoot) {
210
211
  }
211
212
  function resolveConfig(overrides = {}, startDir) {
212
213
  const repoRoot = findRepoRoot2(startDir);
213
- const shared = loadSharedConfig(repoRoot) ?? {};
214
+ const shared = loadSharedConfig2(repoRoot) ?? {};
214
215
  const local = loadLocalConfig(repoRoot) ?? {};
215
216
  const legacy = loadLegacyShellConfig(repoRoot) ?? {};
216
217
  const sources = {
@@ -292,6 +293,12 @@ function resolveConfig(overrides = {}, startDir) {
292
293
  sources
293
294
  };
294
295
  }
296
+ function saveSharedConfig(repoRoot, config) {
297
+ const filePath = path2.join(repoRoot, SHARED_CONFIG_FILE);
298
+ const tmp = `${filePath}.tmp.${process.pid}`;
299
+ fs2.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
300
+ fs2.renameSync(tmp, filePath);
301
+ }
295
302
  function resolvePath(repoRoot, p) {
296
303
  const normalized = normalizeTapPath(p);
297
304
  return path2.isAbsolute(normalized) ? normalized : path2.resolve(repoRoot, normalized);
@@ -753,6 +760,64 @@ async function initCommand(args) {
753
760
  };
754
761
  }
755
762
  logHeader("@hua-labs/tap init");
763
+ const commsRepoIdx = args.indexOf("--comms-repo");
764
+ const commsRepoUrl = commsRepoIdx !== -1 && args[commsRepoIdx + 1] ? args[commsRepoIdx + 1] : void 0;
765
+ if (commsRepoUrl) {
766
+ if (fs6.existsSync(commsDir) && fs6.readdirSync(commsDir).length > 0) {
767
+ const gitDir = path6.join(commsDir, ".git");
768
+ if (fs6.existsSync(gitDir)) {
769
+ log(`Comms directory exists: ${commsDir}`);
770
+ logSuccess("Comms directory is already a git repo \u2014 linking only");
771
+ } else {
772
+ logError(`Comms directory exists but is not a git repo: ${commsDir}`);
773
+ return {
774
+ ok: false,
775
+ command: "init",
776
+ code: "TAP_INIT_CLONE_FAILED",
777
+ message: `Comms directory "${commsDir}" exists but is not a git repo. Remove it or use --force to reinitialize.`,
778
+ warnings: [],
779
+ data: { commsDir, commsRepoUrl }
780
+ };
781
+ }
782
+ } else {
783
+ log(`Cloning comms repo: ${commsRepoUrl}`);
784
+ try {
785
+ execSync(`git clone "${commsRepoUrl}" "${commsDir}"`, {
786
+ stdio: "pipe",
787
+ encoding: "utf-8"
788
+ });
789
+ logSuccess(`Cloned comms repo to ${commsDir}`);
790
+ } catch (err) {
791
+ const msg = err instanceof Error ? err.message : String(err);
792
+ logError(`Failed to clone comms repo: ${msg}`);
793
+ return {
794
+ ok: false,
795
+ command: "init",
796
+ code: "TAP_INIT_CLONE_FAILED",
797
+ message: `Failed to clone comms repo: ${msg}`,
798
+ warnings: [],
799
+ data: { commsRepoUrl }
800
+ };
801
+ }
802
+ }
803
+ }
804
+ {
805
+ const sharedConfig = loadSharedConfig2(repoRoot) ?? {};
806
+ let configChanged = false;
807
+ if (commsRepoUrl) {
808
+ sharedConfig.commsRepoUrl = commsRepoUrl;
809
+ configChanged = true;
810
+ }
811
+ const commsDirRelative = path6.relative(repoRoot, commsDir);
812
+ if (commsDirRelative && commsDirRelative !== "tap-comms") {
813
+ sharedConfig.commsDir = commsDirRelative;
814
+ configChanged = true;
815
+ }
816
+ if (configChanged) {
817
+ saveSharedConfig(repoRoot, sharedConfig);
818
+ logSuccess("Saved comms config to tap-config.json");
819
+ }
820
+ }
756
821
  log(`Comms directory: ${commsDir}`);
757
822
  for (const dir of COMMS_DIRS) {
758
823
  const dirPath = path6.join(commsDir, dir);
@@ -829,7 +894,7 @@ ${entry}
829
894
  // src/adapters/claude.ts
830
895
  import * as fs8 from "fs";
831
896
  import * as path8 from "path";
832
- import { execSync } from "child_process";
897
+ import { execSync as execSync2 } from "child_process";
833
898
 
834
899
  // src/adapters/common.ts
835
900
  import * as fs7 from "fs";
@@ -972,7 +1037,7 @@ function findMcpJsonPath(ctx) {
972
1037
  }
973
1038
  function findClaudeCommand() {
974
1039
  try {
975
- execSync("claude --version", { stdio: "pipe" });
1040
+ execSync2("claude --version", { stdio: "pipe" });
976
1041
  return "claude";
977
1042
  } catch {
978
1043
  return null;
@@ -1834,13 +1899,13 @@ import * as fs13 from "fs";
1834
1899
  import * as net from "net";
1835
1900
  import * as path13 from "path";
1836
1901
  import { randomBytes } from "crypto";
1837
- import { spawn, spawnSync as spawnSync2, execSync as execSync3 } from "child_process";
1902
+ import { spawn, spawnSync as spawnSync2, execSync as execSync4 } from "child_process";
1838
1903
  import { fileURLToPath as fileURLToPath4 } from "url";
1839
1904
 
1840
1905
  // src/runtime/resolve-node.ts
1841
1906
  import * as fs12 from "fs";
1842
1907
  import * as path12 from "path";
1843
- import { execSync as execSync2 } from "child_process";
1908
+ import { execSync as execSync3 } from "child_process";
1844
1909
  function readNodeVersion(repoRoot) {
1845
1910
  const nvFile = path12.join(repoRoot, ".node-version");
1846
1911
  if (!fs12.existsSync(nvFile)) return null;
@@ -1883,7 +1948,7 @@ function probeFnmNode(desiredVersion) {
1883
1948
  );
1884
1949
  if (!fs12.existsSync(candidate)) continue;
1885
1950
  try {
1886
- const v = execSync2(`"${candidate}" --version`, {
1951
+ const v = execSync3(`"${candidate}" --version`, {
1887
1952
  encoding: "utf-8",
1888
1953
  timeout: 5e3
1889
1954
  }).trim();
@@ -1897,7 +1962,7 @@ function probeFnmNode(desiredVersion) {
1897
1962
  }
1898
1963
  function detectNodeMajorVersion(command) {
1899
1964
  try {
1900
- const version2 = execSync2(`"${command}" --version`, {
1965
+ const version2 = execSync3(`"${command}" --version`, {
1901
1966
  encoding: "utf-8",
1902
1967
  timeout: 5e3
1903
1968
  }).trim();
@@ -1911,7 +1976,7 @@ function checkStripTypesSupport(command) {
1911
1976
  const major = detectNodeMajorVersion(command);
1912
1977
  if (major !== null && major >= 22) return true;
1913
1978
  try {
1914
- execSync2(`"${command}" --experimental-strip-types -e ""`, {
1979
+ execSync3(`"${command}" --experimental-strip-types -e ""`, {
1915
1980
  timeout: 5e3,
1916
1981
  stdio: "pipe"
1917
1982
  });
@@ -2469,7 +2534,7 @@ async function terminateProcess(pid, platform) {
2469
2534
  }
2470
2535
  try {
2471
2536
  if (platform === "win32") {
2472
- execSync3(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2537
+ execSync4(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2473
2538
  } else {
2474
2539
  process.kill(pid, "SIGTERM");
2475
2540
  await delay(2e3);
@@ -4543,7 +4608,7 @@ async function bridgeCommand(args) {
4543
4608
  // src/engine/dashboard.ts
4544
4609
  import * as fs15 from "fs";
4545
4610
  import * as path15 from "path";
4546
- import { execSync as execSync4 } from "child_process";
4611
+ import { execSync as execSync5 } from "child_process";
4547
4612
  function collectAgents(commsDir) {
4548
4613
  const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
4549
4614
  if (!fs15.existsSync(heartbeatsPath)) return [];
@@ -4622,7 +4687,7 @@ function collectBridges(repoRoot) {
4622
4687
  }
4623
4688
  function collectPRs() {
4624
4689
  try {
4625
- const output = execSync4(
4690
+ const output = execSync5(
4626
4691
  "gh pr list --state all --limit 10 --json number,title,author,state,url",
4627
4692
  { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
4628
4693
  );
@@ -4869,7 +4934,7 @@ async function serveCommand(args) {
4869
4934
  // src/commands/init-worktree.ts
4870
4935
  import * as fs16 from "fs";
4871
4936
  import * as path17 from "path";
4872
- import { execSync as execSync5 } from "child_process";
4937
+ import { execSync as execSync6 } from "child_process";
4873
4938
  var INIT_WORKTREE_HELP = `
4874
4939
  Usage:
4875
4940
  tap-comms init-worktree [options]
@@ -4893,7 +4958,7 @@ function warn(warnings, message) {
4893
4958
  }
4894
4959
  function run(cmd, opts) {
4895
4960
  try {
4896
- return execSync5(cmd, {
4961
+ return execSync6(cmd, {
4897
4962
  cwd: opts?.cwd,
4898
4963
  encoding: "utf-8",
4899
4964
  stdio: ["pipe", "pipe", "pipe"],
@@ -4910,7 +4975,7 @@ function toAbsolute(p) {
4910
4975
  }
4911
4976
  function probeBun(candidate) {
4912
4977
  try {
4913
- const out = execSync5(`"${candidate}" --version`, {
4978
+ const out = execSync6(`"${candidate}" --version`, {
4914
4979
  encoding: "utf-8",
4915
4980
  stdio: ["pipe", "pipe", "pipe"],
4916
4981
  timeout: 5e3
@@ -4924,7 +4989,7 @@ function findBun() {
4924
4989
  const candidates = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
4925
4990
  for (const name of candidates) {
4926
4991
  try {
4927
- const out = execSync5(
4992
+ const out = execSync6(
4928
4993
  process.platform === "win32" ? `where ${name}` : `which ${name}`,
4929
4994
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
4930
4995
  ).trim();
@@ -5376,7 +5441,7 @@ async function dashboardCommand(args) {
5376
5441
  import {
5377
5442
  existsSync as existsSync16,
5378
5443
  mkdirSync as mkdirSync10,
5379
- readdirSync as readdirSync3,
5444
+ readdirSync as readdirSync4,
5380
5445
  readFileSync as readFileSync14,
5381
5446
  statSync as statSync2,
5382
5447
  unlinkSync as unlinkSync3
@@ -5388,7 +5453,7 @@ var FAIL = "fail";
5388
5453
  function countFiles(dir, ext = ".md") {
5389
5454
  if (!existsSync16(dir)) return 0;
5390
5455
  try {
5391
- return readdirSync3(dir).filter((f) => f.endsWith(ext)).length;
5456
+ return readdirSync4(dir).filter((f) => f.endsWith(ext)).length;
5392
5457
  } catch {
5393
5458
  return 0;
5394
5459
  }
@@ -5398,7 +5463,7 @@ function recentFileCount(dir, withinMs) {
5398
5463
  const cutoff = Date.now() - withinMs;
5399
5464
  let count = 0;
5400
5465
  try {
5401
- for (const f of readdirSync3(dir)) {
5466
+ for (const f of readdirSync4(dir)) {
5402
5467
  if (!f.endsWith(".md")) continue;
5403
5468
  try {
5404
5469
  if (statSync2(join17(dir, f)).mtimeMs > cutoff) count++;
@@ -5409,6 +5474,21 @@ function recentFileCount(dir, withinMs) {
5409
5474
  }
5410
5475
  return count;
5411
5476
  }
5477
+ function loadBridgeRuntimeHeartbeat(bridgeState) {
5478
+ const runtimeStateDir = bridgeState?.runtimeStateDir;
5479
+ if (!runtimeStateDir) {
5480
+ return null;
5481
+ }
5482
+ const heartbeatPath = join17(runtimeStateDir, "heartbeat.json");
5483
+ if (!existsSync16(heartbeatPath)) {
5484
+ return null;
5485
+ }
5486
+ try {
5487
+ return JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
5488
+ } catch {
5489
+ return null;
5490
+ }
5491
+ }
5412
5492
  function checkComms(commsDir) {
5413
5493
  const checks = [];
5414
5494
  checks.push({
@@ -5492,6 +5572,7 @@ function checkInstances(repoRoot, stateDir) {
5492
5572
  const running = isBridgeRunning(stateDir, id);
5493
5573
  const bridgeState = loadBridgeState(stateDir, id);
5494
5574
  const heartbeatAge = getHeartbeatAge(stateDir, id);
5575
+ const runtimeHeartbeat = loadBridgeRuntimeHeartbeat(bridgeState);
5495
5576
  let status;
5496
5577
  let message;
5497
5578
  let fix;
@@ -5535,6 +5616,11 @@ function checkInstances(repoRoot, stateDir) {
5535
5616
  status = WARN;
5536
5617
  message = "Not running";
5537
5618
  }
5619
+ const lastRuntimeError = runtimeHeartbeat?.lastError?.trim();
5620
+ if (lastRuntimeError) {
5621
+ status = status === FAIL ? FAIL : WARN;
5622
+ message = `${message}; bridge last error: ${lastRuntimeError}`;
5623
+ }
5538
5624
  checks.push({ name: `bridge: ${id}`, status, message, fix });
5539
5625
  } else {
5540
5626
  checks.push({
@@ -5737,6 +5823,158 @@ async function doctorCommand(args) {
5737
5823
  };
5738
5824
  }
5739
5825
 
5826
+ // src/commands/comms.ts
5827
+ import { execSync as execSync7 } from "child_process";
5828
+ import * as fs17 from "fs";
5829
+ import * as path18 from "path";
5830
+ var COMMS_HELP = `
5831
+ Usage:
5832
+ tap-comms comms <subcommand>
5833
+
5834
+ Subcommands:
5835
+ pull Pull latest changes from comms remote repo
5836
+ push Commit and push comms changes to remote repo
5837
+
5838
+ Examples:
5839
+ npx @hua-labs/tap comms pull
5840
+ npx @hua-labs/tap comms push
5841
+ `.trim();
5842
+ function isGitRepo(dir) {
5843
+ return fs17.existsSync(path18.join(dir, ".git"));
5844
+ }
5845
+ function commsPull(commsDir) {
5846
+ logHeader("tap comms pull");
5847
+ if (!isGitRepo(commsDir)) {
5848
+ logError(`${commsDir} is not a git repository`);
5849
+ return {
5850
+ ok: false,
5851
+ command: "comms",
5852
+ code: "TAP_COMMS_NOT_REPO",
5853
+ message: `Comms directory is not a git repo. Use 'tap init --comms-repo <url>' to set up.`,
5854
+ warnings: [],
5855
+ data: { commsDir }
5856
+ };
5857
+ }
5858
+ try {
5859
+ const output = execSync7("git pull --rebase", {
5860
+ cwd: commsDir,
5861
+ encoding: "utf-8",
5862
+ stdio: "pipe"
5863
+ });
5864
+ logSuccess("Comms pull complete");
5865
+ if (output.trim()) log(output.trim());
5866
+ return {
5867
+ ok: true,
5868
+ command: "comms",
5869
+ code: "TAP_COMMS_PULL_OK",
5870
+ message: "Comms pull complete",
5871
+ warnings: [],
5872
+ data: { commsDir }
5873
+ };
5874
+ } catch (err) {
5875
+ const msg = err instanceof Error ? err.message : String(err);
5876
+ logError(`Pull failed: ${msg}`);
5877
+ return {
5878
+ ok: false,
5879
+ command: "comms",
5880
+ code: "TAP_COMMS_PULL_FAILED",
5881
+ message: `Pull failed: ${msg}`,
5882
+ warnings: [],
5883
+ data: { commsDir }
5884
+ };
5885
+ }
5886
+ }
5887
+ function commsPush(commsDir) {
5888
+ logHeader("tap comms push");
5889
+ if (!isGitRepo(commsDir)) {
5890
+ logError(`${commsDir} is not a git repository`);
5891
+ return {
5892
+ ok: false,
5893
+ command: "comms",
5894
+ code: "TAP_COMMS_NOT_REPO",
5895
+ message: `Comms directory is not a git repo. Use 'tap init --comms-repo <url>' to set up.`,
5896
+ warnings: [],
5897
+ data: { commsDir }
5898
+ };
5899
+ }
5900
+ try {
5901
+ execSync7("git add -A", { cwd: commsDir, stdio: "pipe" });
5902
+ const status = execSync7("git status --porcelain", {
5903
+ cwd: commsDir,
5904
+ encoding: "utf-8",
5905
+ stdio: "pipe"
5906
+ }).trim();
5907
+ if (!status) {
5908
+ log("Nothing to push \u2014 comms directory is clean");
5909
+ return {
5910
+ ok: true,
5911
+ command: "comms",
5912
+ code: "TAP_COMMS_PUSH_OK",
5913
+ message: "Nothing to push",
5914
+ warnings: [],
5915
+ data: { commsDir, changed: false }
5916
+ };
5917
+ }
5918
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5919
+ execSync7(`git commit -m "chore(comms): sync ${timestamp}"`, {
5920
+ cwd: commsDir,
5921
+ stdio: "pipe"
5922
+ });
5923
+ execSync7("git push", { cwd: commsDir, stdio: "pipe" });
5924
+ logSuccess("Comms push complete");
5925
+ return {
5926
+ ok: true,
5927
+ command: "comms",
5928
+ code: "TAP_COMMS_PUSH_OK",
5929
+ message: "Comms push complete",
5930
+ warnings: [],
5931
+ data: { commsDir, changed: true }
5932
+ };
5933
+ } catch (err) {
5934
+ const msg = err instanceof Error ? err.message : String(err);
5935
+ logError(`Push failed: ${msg}`);
5936
+ return {
5937
+ ok: false,
5938
+ command: "comms",
5939
+ code: "TAP_COMMS_PUSH_FAILED",
5940
+ message: `Push failed: ${msg}`,
5941
+ warnings: [],
5942
+ data: { commsDir }
5943
+ };
5944
+ }
5945
+ }
5946
+ async function commsCommand(args) {
5947
+ const subcommand = args[0];
5948
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
5949
+ log(COMMS_HELP);
5950
+ return {
5951
+ ok: true,
5952
+ command: "comms",
5953
+ code: "TAP_NO_OP",
5954
+ message: COMMS_HELP,
5955
+ warnings: [],
5956
+ data: {}
5957
+ };
5958
+ }
5959
+ const repoRoot = findRepoRoot();
5960
+ const commsDir = resolveCommsDir(args, repoRoot);
5961
+ switch (subcommand) {
5962
+ case "pull":
5963
+ return commsPull(commsDir);
5964
+ case "push":
5965
+ return commsPush(commsDir);
5966
+ default:
5967
+ return {
5968
+ ok: false,
5969
+ command: "comms",
5970
+ code: "TAP_INVALID_ARGUMENT",
5971
+ message: `Unknown comms subcommand: ${subcommand}. Use pull or push.`,
5972
+ warnings: [],
5973
+ data: {}
5974
+ };
5975
+ }
5976
+ }
5977
+
5740
5978
  // src/output.ts
5741
5979
  function emitResult(result, jsonMode) {
5742
5980
  if (jsonMode) {
@@ -5777,6 +6015,7 @@ Commands:
5777
6015
  bridge <sub> [inst] Manage bridges (start, stop, status)
5778
6016
  up Start all registered bridge daemons
5779
6017
  down Stop all running bridge daemons
6018
+ comms <pull|push> Sync comms directory with remote repo
5780
6019
  dashboard Show unified ops dashboard
5781
6020
  doctor Diagnose tap infrastructure health
5782
6021
  serve Start tap-comms MCP server (stdio)
@@ -5804,6 +6043,7 @@ function normalizeCommandName(command) {
5804
6043
  case "bridge":
5805
6044
  case "up":
5806
6045
  case "down":
6046
+ case "comms":
5807
6047
  case "dashboard":
5808
6048
  case "doctor":
5809
6049
  case "serve":
@@ -5861,6 +6101,9 @@ async function main() {
5861
6101
  case "down":
5862
6102
  result = await downCommand(commandArgs);
5863
6103
  break;
6104
+ case "comms":
6105
+ result = await commsCommand(commandArgs);
6106
+ break;
5864
6107
  case "dashboard":
5865
6108
  result = await dashboardCommand(commandArgs);
5866
6109
  break;