@agent-team-foundation/first-tree-hub 0.7.1 → 0.7.2

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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { C as setConfigValue, S as serverConfigSchema, _ as loadAgents, b as resetConfigMeta, c as saveCredentials, f as agentConfigSchema, g as initConfig, h as getConfigValue, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, v as readConfigFile, y as resetConfig } from "../bootstrap-CRDR6NwE.mjs";
3
- import { A as stopPostgres, C as checkServerReachable, F as SdkError, I as SessionRegistry, L as cleanWorkspaces, M as createOwner, P as FirstTreeHubSDK, S as checkServerHealth, T as printResults, _ as checkClientConfig, a as uninstallClientService, b as checkNodeVersion, c as promptAddAgent, d as loadOnboardState, f as onboardCheck, g as checkAgentConfigs, h as runMigrations, j as ClientRuntime, l as promptMissingFields, m as saveOnboardState, n as installClientService, o as startServer, p as onboardCreate, r as isServiceSupported, s as isInteractive, t as getClientServiceStatus, u as formatCheckReport, v as checkDatabase, w as checkWebSocket, x as checkServerConfig, y as checkDocker } from "../core-C05B8FzH.mjs";
3
+ import { A as stopPostgres, C as checkServerReachable, F as SdkError, I as SessionRegistry, L as cleanWorkspaces, M as createOwner, P as FirstTreeHubSDK, S as checkServerHealth, T as printResults, _ as checkClientConfig, a as uninstallClientService, b as checkNodeVersion, c as promptAddAgent, d as loadOnboardState, f as onboardCheck, g as checkAgentConfigs, h as runMigrations, j as ClientRuntime, l as promptMissingFields, m as saveOnboardState, n as installClientService, o as startServer, p as onboardCreate, r as isServiceSupported, s as isInteractive, t as getClientServiceStatus, u as formatCheckReport, v as checkDatabase, w as checkWebSocket, x as checkServerConfig, y as checkDocker } from "../core-6-paFwyo.mjs";
4
4
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-CJ08ntOD.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { Command } from "commander";
@@ -1493,6 +1493,451 @@ echo "long message body" | first-tree-hub agent send <agentId>
1493
1493
  For content with quotes, \`$\`, backticks, or newlines, prefer stdin to avoid shell escaping issues.
1494
1494
  `;
1495
1495
  }
1496
+ const DEFAULT_CLONE_TIMEOUT_MS = 300 * 1e3;
1497
+ const FETCH_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
1498
+ const SESSION_BRANCH_PREFIX = "hub-session";
1499
+ function hashUrl(url) {
1500
+ return createHash("sha256").update(url).digest("hex").slice(0, 32);
1501
+ }
1502
+ function shortHash(input) {
1503
+ return createHash("sha256").update(input).digest("hex").slice(0, 8);
1504
+ }
1505
+ function deriveSessionBranchName(sessionKey, url) {
1506
+ return `${SESSION_BRANCH_PREFIX}-${shortHash(sessionKey)}-${shortHash(url)}`;
1507
+ }
1508
+ /**
1509
+ * A value is SHA-like when it's a 7–40 character hex string. Used to decide
1510
+ * whether `ref` should be resolved via the remote namespace (branch name) or
1511
+ * used as-is (commit hash).
1512
+ */
1513
+ function looksLikeCommitSha(ref) {
1514
+ return /^[0-9a-f]{7,40}$/i.test(ref);
1515
+ }
1516
+ function createGitMirrorManager(opts) {
1517
+ const mirrorsRoot = join(opts.dataDir, "git-mirrors");
1518
+ const cloneTimeoutMs = opts.cloneTimeoutMs ?? Number(process.env.FIRST_TREE_HUB_GIT_CLONE_TIMEOUT_MS ?? DEFAULT_CLONE_TIMEOUT_MS);
1519
+ const log = opts.log ?? (() => {});
1520
+ const urlLocks = /* @__PURE__ */ new Map();
1521
+ function withUrlLock(url, op) {
1522
+ const key = hashUrl(url);
1523
+ const next = (urlLocks.get(key) ?? Promise.resolve()).then(op, op);
1524
+ urlLocks.set(key, next);
1525
+ next.then(() => {
1526
+ if (urlLocks.get(key) === next) urlLocks.delete(key);
1527
+ }, () => {
1528
+ if (urlLocks.get(key) === next) urlLocks.delete(key);
1529
+ });
1530
+ return next;
1531
+ }
1532
+ function mirrorDir(url) {
1533
+ return join(mirrorsRoot, hashUrl(url));
1534
+ }
1535
+ async function git(args, cwd, timeoutMs, env) {
1536
+ const start = Date.now();
1537
+ return await new Promise((resolveExec, rejectExec) => {
1538
+ const proc = spawn("git", args, {
1539
+ cwd: cwd ?? void 0,
1540
+ env: env ?? process.env,
1541
+ stdio: [
1542
+ "ignore",
1543
+ "pipe",
1544
+ "pipe"
1545
+ ]
1546
+ });
1547
+ let stdout = "";
1548
+ let stderr = "";
1549
+ proc.stdout.on("data", (d) => {
1550
+ stdout += String(d);
1551
+ });
1552
+ proc.stderr.on("data", (d) => {
1553
+ stderr += String(d);
1554
+ });
1555
+ const timer = setTimeout(() => {
1556
+ proc.kill("SIGKILL");
1557
+ rejectExec(new GitMirrorTimeoutError(`git ${args.join(" ")} timed out after ${timeoutMs}ms`));
1558
+ }, timeoutMs);
1559
+ proc.on("error", (err) => {
1560
+ clearTimeout(timer);
1561
+ rejectExec(err);
1562
+ });
1563
+ proc.on("close", (code) => {
1564
+ clearTimeout(timer);
1565
+ const elapsedMs = Date.now() - start;
1566
+ if (code === 0) resolveExec({
1567
+ stdout,
1568
+ stderr,
1569
+ elapsedMs
1570
+ });
1571
+ else rejectExec(new GitMirrorError(`git ${args.join(" ")} exited with code ${code}: ${stderr.slice(0, 1024)}`));
1572
+ });
1573
+ });
1574
+ }
1575
+ async function gitOk(args, cwd, timeoutMs) {
1576
+ try {
1577
+ await git(args, cwd, timeoutMs);
1578
+ return true;
1579
+ } catch {
1580
+ return false;
1581
+ }
1582
+ }
1583
+ /**
1584
+ * Bring the mirror's config to the invariant expected by this module:
1585
+ * fetch refspec = `+refs/heads/*:refs/remotes/origin/*`, `remote.origin.mirror`
1586
+ * absent, `refs/remotes/origin/HEAD` resolvable.
1587
+ *
1588
+ * Called from `ensureMirror` on every invocation — both the fresh-clone path
1589
+ * (ensures our own bootstrap wrote the right values) and the pre-existing
1590
+ * mirror path (repairs drift from the legacy `--mirror` config).
1591
+ */
1592
+ async function assertMirrorConfig(mirrorPath, url) {
1593
+ let migrated = false;
1594
+ let currentFetch = "";
1595
+ try {
1596
+ const { stdout } = await git([
1597
+ "config",
1598
+ "--get-all",
1599
+ "remote.origin.fetch"
1600
+ ], mirrorPath, 1e4);
1601
+ currentFetch = stdout.trim();
1602
+ } catch {
1603
+ currentFetch = "";
1604
+ }
1605
+ if (currentFetch !== FETCH_REFSPEC) {
1606
+ await git([
1607
+ "config",
1608
+ "--replace-all",
1609
+ "remote.origin.fetch",
1610
+ FETCH_REFSPEC
1611
+ ], mirrorPath, 1e4);
1612
+ migrated = true;
1613
+ }
1614
+ if (await gitOk([
1615
+ "config",
1616
+ "--get",
1617
+ "remote.origin.mirror"
1618
+ ], mirrorPath, 1e4)) {
1619
+ await git([
1620
+ "config",
1621
+ "--unset-all",
1622
+ "remote.origin.mirror"
1623
+ ], mirrorPath, 1e4);
1624
+ migrated = true;
1625
+ }
1626
+ try {
1627
+ const { stdout } = await git([
1628
+ "config",
1629
+ "--get",
1630
+ "remote.origin.url"
1631
+ ], mirrorPath, 1e4);
1632
+ if (stdout.trim() !== url) {
1633
+ await git([
1634
+ "config",
1635
+ "--replace-all",
1636
+ "remote.origin.url",
1637
+ url
1638
+ ], mirrorPath, 1e4);
1639
+ migrated = true;
1640
+ }
1641
+ } catch {
1642
+ await git([
1643
+ "remote",
1644
+ "add",
1645
+ "origin",
1646
+ url
1647
+ ], mirrorPath, 1e4);
1648
+ migrated = true;
1649
+ }
1650
+ if (migrated) {
1651
+ await git([
1652
+ "fetch",
1653
+ "--prune",
1654
+ "origin"
1655
+ ], mirrorPath, cloneTimeoutMs);
1656
+ await gitOk([
1657
+ "remote",
1658
+ "set-head",
1659
+ "origin",
1660
+ "--auto"
1661
+ ], mirrorPath, 3e4);
1662
+ log("mirrorConfigMigrated", { gitUrl: url });
1663
+ }
1664
+ return { migrated };
1665
+ }
1666
+ /**
1667
+ * Bootstrap a fresh mirror at `mirrorPath`. Uses `git init --bare` +
1668
+ * manual remote setup rather than `git clone --mirror` / `git clone --bare`,
1669
+ * so we never transiently have the mirror configured to force-write
1670
+ * `refs/heads/*` on fetch.
1671
+ */
1672
+ async function bootstrapMirror(mirrorPath, url) {
1673
+ mkdirSync(dirname(mirrorPath), { recursive: true });
1674
+ await git([
1675
+ "init",
1676
+ "--bare",
1677
+ mirrorPath
1678
+ ], null, cloneTimeoutMs);
1679
+ await git([
1680
+ "remote",
1681
+ "add",
1682
+ "origin",
1683
+ url
1684
+ ], mirrorPath, 1e4);
1685
+ await git([
1686
+ "config",
1687
+ "--replace-all",
1688
+ "remote.origin.fetch",
1689
+ FETCH_REFSPEC
1690
+ ], mirrorPath, 1e4);
1691
+ await git([
1692
+ "fetch",
1693
+ "--prune",
1694
+ "origin"
1695
+ ], mirrorPath, cloneTimeoutMs);
1696
+ await gitOk([
1697
+ "remote",
1698
+ "set-head",
1699
+ "origin",
1700
+ "--auto"
1701
+ ], mirrorPath, 3e4);
1702
+ }
1703
+ async function branchExists(mirrorPath, branchName) {
1704
+ return await gitOk([
1705
+ "rev-parse",
1706
+ "--verify",
1707
+ "--quiet",
1708
+ `refs/heads/${branchName}`
1709
+ ], mirrorPath, 1e4);
1710
+ }
1711
+ /**
1712
+ * Resolve the commit-ish to base a new session branch on.
1713
+ *
1714
+ * - explicit SHA → use as-is
1715
+ * - explicit branch name → prefer `refs/remotes/origin/<ref>`, fall back to
1716
+ * a literal SHA resolution in case the caller handed us a short commit
1717
+ * - `ref` absent → `refs/remotes/origin/HEAD`
1718
+ */
1719
+ async function resolveBase(mirrorPath, ref) {
1720
+ if (!ref) {
1721
+ if (await gitOk([
1722
+ "rev-parse",
1723
+ "--verify",
1724
+ "--quiet",
1725
+ "refs/remotes/origin/HEAD"
1726
+ ], mirrorPath, 1e4)) return "refs/remotes/origin/HEAD";
1727
+ throw new GitMirrorError("Cannot resolve default branch: refs/remotes/origin/HEAD is missing. Re-run with an explicit `ref`.");
1728
+ }
1729
+ if (looksLikeCommitSha(ref)) {
1730
+ if (await gitOk([
1731
+ "cat-file",
1732
+ "-e",
1733
+ ref
1734
+ ], mirrorPath, 1e4)) return ref;
1735
+ }
1736
+ const remoteRef = `refs/remotes/origin/${ref}`;
1737
+ if (await gitOk([
1738
+ "rev-parse",
1739
+ "--verify",
1740
+ "--quiet",
1741
+ remoteRef
1742
+ ], mirrorPath, 1e4)) return remoteRef;
1743
+ return ref;
1744
+ }
1745
+ return {
1746
+ get mirrorsRoot() {
1747
+ return mirrorsRoot;
1748
+ },
1749
+ ensureMirror(url) {
1750
+ return withUrlLock(url, async () => {
1751
+ mkdirSync(mirrorsRoot, { recursive: true });
1752
+ const path = mirrorDir(url);
1753
+ if (existsSync(join(path, "HEAD"))) {
1754
+ const { migrated } = await assertMirrorConfig(path, url);
1755
+ if (migrated) {}
1756
+ return {
1757
+ mirrorPath: path,
1758
+ elapsedMs: 0,
1759
+ cloned: false
1760
+ };
1761
+ }
1762
+ const start = Date.now();
1763
+ try {
1764
+ await bootstrapMirror(path, url);
1765
+ const elapsedMs = Date.now() - start;
1766
+ log("ensureMirror", {
1767
+ gitUrl: url,
1768
+ elapsedMs,
1769
+ cloned: true
1770
+ });
1771
+ return {
1772
+ mirrorPath: path,
1773
+ elapsedMs,
1774
+ cloned: true
1775
+ };
1776
+ } catch (err) {
1777
+ if (err instanceof GitMirrorTimeoutError) log("mirrorCloneTimeout", {
1778
+ gitUrl: url,
1779
+ timeoutMs: cloneTimeoutMs,
1780
+ elapsedMs: cloneTimeoutMs
1781
+ });
1782
+ if (existsSync(path)) rmSync(path, {
1783
+ recursive: true,
1784
+ force: true
1785
+ });
1786
+ throw err;
1787
+ }
1788
+ });
1789
+ },
1790
+ fetchMirror(url) {
1791
+ return withUrlLock(url, async () => {
1792
+ const path = mirrorDir(url);
1793
+ if (!existsSync(join(path, "HEAD"))) throw new GitMirrorError(`Cannot fetch — no mirror exists for "${url}"`);
1794
+ try {
1795
+ const { elapsedMs } = await git([
1796
+ "fetch",
1797
+ "--prune",
1798
+ "origin"
1799
+ ], path, cloneTimeoutMs);
1800
+ return { elapsedMs };
1801
+ } catch (err) {
1802
+ log("mirrorFetchFailed", {
1803
+ gitUrl: url,
1804
+ errorCode: err instanceof GitMirrorError ? "git-failed" : "unknown",
1805
+ stderr: err instanceof Error ? err.message.slice(0, 1024) : String(err).slice(0, 1024)
1806
+ });
1807
+ throw err;
1808
+ }
1809
+ });
1810
+ },
1811
+ createWorktree({ url, ref, targetPath, sessionKey }) {
1812
+ return withUrlLock(url, async () => {
1813
+ const mirror = mirrorDir(url);
1814
+ if (!existsSync(join(mirror, "HEAD"))) throw new GitMirrorError(`Cannot create worktree — no mirror exists for "${url}"`);
1815
+ const absTarget = resolve(targetPath);
1816
+ const branchName = deriveSessionBranchName(sessionKey, url);
1817
+ if (existsSync(absTarget) && !isHubManagedWorktree(absTarget)) {
1818
+ log("worktreeCreateConflict", {
1819
+ gitUrl: url,
1820
+ targetPath: absTarget,
1821
+ occupantKind: classifyOccupant(absTarget)
1822
+ });
1823
+ throw new GitMirrorWorktreeConflictError(`Worktree target "${absTarget}" is already occupied by ${classifyOccupant(absTarget)} — aborting (D13)`);
1824
+ }
1825
+ const pathExists = existsSync(absTarget);
1826
+ const hasBranch = await branchExists(mirror, branchName);
1827
+ mkdirSync(dirname(absTarget), { recursive: true });
1828
+ if (pathExists && hasBranch) {} else if (pathExists && !hasBranch) throw new GitMirrorError(`Worktree directory "${absTarget}" exists as a Hub worktree but the expected session branch "${branchName}" is missing in the mirror — manual cleanup required`);
1829
+ else if (!pathExists && hasBranch) await git([
1830
+ "worktree",
1831
+ "add",
1832
+ absTarget,
1833
+ branchName
1834
+ ], mirror, cloneTimeoutMs);
1835
+ else await git([
1836
+ "worktree",
1837
+ "add",
1838
+ "-b",
1839
+ branchName,
1840
+ absTarget,
1841
+ await resolveBase(mirror, ref)
1842
+ ], mirror, cloneTimeoutMs);
1843
+ return {
1844
+ worktreePath: absTarget,
1845
+ headCommit: (await git(["rev-parse", "HEAD"], absTarget, 3e4)).stdout.trim(),
1846
+ branchName
1847
+ };
1848
+ });
1849
+ },
1850
+ removeWorktree({ url, path, branchName }) {
1851
+ return withUrlLock(url, async () => {
1852
+ const absTarget = resolve(path);
1853
+ const mirror = mirrorDir(url);
1854
+ if (!isBareRepo(mirror)) {
1855
+ if (existsSync(absTarget)) rmSync(absTarget, {
1856
+ recursive: true,
1857
+ force: true
1858
+ });
1859
+ return;
1860
+ }
1861
+ if (existsSync(absTarget)) await gitOk([
1862
+ "worktree",
1863
+ "remove",
1864
+ "--force",
1865
+ absTarget
1866
+ ], mirror, 3e4);
1867
+ else await gitOk(["worktree", "prune"], mirror, 3e4);
1868
+ if (existsSync(absTarget)) rmSync(absTarget, {
1869
+ recursive: true,
1870
+ force: true
1871
+ });
1872
+ if (await branchExists(mirror, branchName)) await gitOk([
1873
+ "branch",
1874
+ "-D",
1875
+ branchName
1876
+ ], mirror, 1e4);
1877
+ });
1878
+ },
1879
+ async gcMirrors(stillReferencedUrls) {
1880
+ if (!existsSync(mirrorsRoot)) return { removed: [] };
1881
+ const wantedHashes = new Set([...stillReferencedUrls].map(hashUrl));
1882
+ const removed = [];
1883
+ for (const entry of readdirSync(mirrorsRoot)) {
1884
+ if (wantedHashes.has(entry)) continue;
1885
+ const path = join(mirrorsRoot, entry);
1886
+ if (!isBareRepo(path)) continue;
1887
+ rmSync(path, {
1888
+ recursive: true,
1889
+ force: true
1890
+ });
1891
+ removed.push(entry);
1892
+ }
1893
+ return { removed };
1894
+ }
1895
+ };
1896
+ }
1897
+ function isBareRepo(p) {
1898
+ return existsSync(join(p, "HEAD")) && existsSync(join(p, "objects"));
1899
+ }
1900
+ function isHubManagedWorktree(p) {
1901
+ const gitMarker = join(p, ".git");
1902
+ if (!existsSync(gitMarker)) return false;
1903
+ try {
1904
+ return statSync(gitMarker).isFile();
1905
+ } catch {
1906
+ return false;
1907
+ }
1908
+ }
1909
+ function classifyOccupant(p) {
1910
+ try {
1911
+ const stat = statSync(p);
1912
+ if (stat.isSymbolicLink()) return "symlink";
1913
+ if (stat.isDirectory()) {
1914
+ if (existsSync(join(p, ".git"))) return "git-repo";
1915
+ return "directory";
1916
+ }
1917
+ if (stat.isFile()) return "file";
1918
+ return "other";
1919
+ } catch {
1920
+ return "unknown";
1921
+ }
1922
+ }
1923
+ var GitMirrorError = class extends Error {
1924
+ constructor(message) {
1925
+ super(message);
1926
+ this.name = "GitMirrorError";
1927
+ }
1928
+ };
1929
+ var GitMirrorTimeoutError = class extends GitMirrorError {
1930
+ constructor(message) {
1931
+ super(message);
1932
+ this.name = "GitMirrorTimeoutError";
1933
+ }
1934
+ };
1935
+ var GitMirrorWorktreeConflictError = class extends GitMirrorError {
1936
+ constructor(message) {
1937
+ super(message);
1938
+ this.name = "GitMirrorWorktreeConflictError";
1939
+ }
1940
+ };
1496
1941
  /**
1497
1942
  * InputController — push-based async iterable bridge.
1498
1943
  *
@@ -1741,7 +2186,7 @@ const createClaudeCodeHandler = (config) => {
1741
2186
  let appliedConfigVersion = 0;
1742
2187
  let appliedModel = "";
1743
2188
  let appliedPayload = null;
1744
- /** Worktree paths materialised for this session — removed on shutdown. */
2189
+ /** Worktrees materialised for this session — each entry removed on shutdown. */
1745
2190
  const ownedWorktrees = [];
1746
2191
  function toSDKUserMessage(message, sessionId) {
1747
2192
  const rawContent = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
@@ -1965,28 +2410,37 @@ const createClaudeCodeHandler = (config) => {
1965
2410
  await gitMirrorManager.fetchMirror(repo.url);
1966
2411
  if (existsSync(targetPath) && isHubWorktreeMarker(targetPath)) {
1967
2412
  sessionCtx.log(`Git: reusing existing worktree at ${localPath}`);
1968
- ownedWorktrees.push(targetPath);
2413
+ ownedWorktrees.push({
2414
+ url: repo.url,
2415
+ path: targetPath,
2416
+ branchName: deriveSessionBranchName(sessionCtx.chatId, repo.url)
2417
+ });
1969
2418
  continue;
1970
2419
  }
1971
- const { headCommit } = await gitMirrorManager.createWorktree({
2420
+ const { headCommit, branchName } = await gitMirrorManager.createWorktree({
1972
2421
  url: repo.url,
1973
2422
  ref: repo.ref,
1974
- targetPath
2423
+ targetPath,
2424
+ sessionKey: sessionCtx.chatId
1975
2425
  });
1976
- ownedWorktrees.push(targetPath);
1977
- sessionCtx.log(`Git: worktree at ${localPath} @ ${headCommit.slice(0, 7)}`);
2426
+ ownedWorktrees.push({
2427
+ url: repo.url,
2428
+ path: targetPath,
2429
+ branchName
2430
+ });
2431
+ sessionCtx.log(`Git: worktree at ${localPath} @ ${headCommit.slice(0, 7)} on ${branchName}`);
1978
2432
  }
1979
2433
  }
1980
2434
  /** Tear down all worktrees this session owns; best-effort. */
1981
2435
  async function cleanupGitWorktrees(sessionCtx) {
1982
2436
  if (!gitMirrorManager) return;
1983
2437
  while (ownedWorktrees.length > 0) {
1984
- const path = ownedWorktrees.pop();
1985
- if (!path) continue;
2438
+ const entry = ownedWorktrees.pop();
2439
+ if (!entry) continue;
1986
2440
  try {
1987
- await gitMirrorManager.removeWorktree(path);
2441
+ await gitMirrorManager.removeWorktree(entry);
1988
2442
  } catch (err) {
1989
- sessionCtx.log(`Git: removeWorktree(${path}) failed — ${err instanceof Error ? err.message : String(err)}`);
2443
+ sessionCtx.log(`Git: removeWorktree(${entry.path}) failed — ${err instanceof Error ? err.message : String(err)}`);
1990
2444
  }
1991
2445
  }
1992
2446
  }
@@ -2200,226 +2654,6 @@ function createAgentConfigCache(opts) {
2200
2654
  }
2201
2655
  };
2202
2656
  }
2203
- const DEFAULT_CLONE_TIMEOUT_MS = 300 * 1e3;
2204
- function hashUrl(url) {
2205
- return createHash("sha256").update(url).digest("hex").slice(0, 32);
2206
- }
2207
- function createGitMirrorManager(opts) {
2208
- const mirrorsRoot = join(opts.dataDir, "git-mirrors");
2209
- const cloneTimeoutMs = opts.cloneTimeoutMs ?? Number(process.env.FIRST_TREE_HUB_GIT_CLONE_TIMEOUT_MS ?? DEFAULT_CLONE_TIMEOUT_MS);
2210
- const log = opts.log ?? (() => {});
2211
- function mirrorDir(url) {
2212
- return join(mirrorsRoot, hashUrl(url));
2213
- }
2214
- async function git(args, cwd, timeoutMs, env) {
2215
- const start = Date.now();
2216
- return await new Promise((resolveExec, rejectExec) => {
2217
- const proc = spawn("git", args, {
2218
- cwd: cwd ?? void 0,
2219
- env: env ?? process.env,
2220
- stdio: [
2221
- "ignore",
2222
- "pipe",
2223
- "pipe"
2224
- ]
2225
- });
2226
- let stdout = "";
2227
- let stderr = "";
2228
- proc.stdout.on("data", (d) => {
2229
- stdout += String(d);
2230
- });
2231
- proc.stderr.on("data", (d) => {
2232
- stderr += String(d);
2233
- });
2234
- const timer = setTimeout(() => {
2235
- proc.kill("SIGKILL");
2236
- rejectExec(new GitMirrorTimeoutError(`git ${args.join(" ")} timed out after ${timeoutMs}ms`));
2237
- }, timeoutMs);
2238
- proc.on("error", (err) => {
2239
- clearTimeout(timer);
2240
- rejectExec(err);
2241
- });
2242
- proc.on("close", (code) => {
2243
- clearTimeout(timer);
2244
- const elapsedMs = Date.now() - start;
2245
- if (code === 0) resolveExec({
2246
- stdout,
2247
- stderr,
2248
- elapsedMs
2249
- });
2250
- else rejectExec(new GitMirrorError(`git ${args.join(" ")} exited with code ${code}: ${stderr.slice(0, 1024)}`));
2251
- });
2252
- });
2253
- }
2254
- return {
2255
- get mirrorsRoot() {
2256
- return mirrorsRoot;
2257
- },
2258
- async ensureMirror(url) {
2259
- mkdirSync(mirrorsRoot, { recursive: true });
2260
- const path = mirrorDir(url);
2261
- if (existsSync(join(path, "HEAD"))) return {
2262
- mirrorPath: path,
2263
- elapsedMs: 0,
2264
- cloned: false
2265
- };
2266
- try {
2267
- const { elapsedMs } = await git([
2268
- "clone",
2269
- "--mirror",
2270
- url,
2271
- path
2272
- ], null, cloneTimeoutMs);
2273
- log("ensureMirror", {
2274
- gitUrl: url,
2275
- elapsedMs,
2276
- cloned: true
2277
- });
2278
- return {
2279
- mirrorPath: path,
2280
- elapsedMs,
2281
- cloned: true
2282
- };
2283
- } catch (err) {
2284
- if (err instanceof GitMirrorTimeoutError) log("mirrorCloneTimeout", {
2285
- gitUrl: url,
2286
- timeoutMs: cloneTimeoutMs,
2287
- elapsedMs: cloneTimeoutMs
2288
- });
2289
- if (existsSync(path)) rmSync(path, {
2290
- recursive: true,
2291
- force: true
2292
- });
2293
- throw err;
2294
- }
2295
- },
2296
- async fetchMirror(url) {
2297
- const path = mirrorDir(url);
2298
- if (!existsSync(join(path, "HEAD"))) throw new GitMirrorError(`Cannot fetch — no mirror exists for "${url}"`);
2299
- try {
2300
- const { elapsedMs } = await git(["fetch", "--prune"], path, cloneTimeoutMs);
2301
- return { elapsedMs };
2302
- } catch (err) {
2303
- log("mirrorFetchFailed", {
2304
- gitUrl: url,
2305
- errorCode: err instanceof GitMirrorError ? "git-failed" : "unknown",
2306
- stderr: err instanceof Error ? err.message.slice(0, 1024) : String(err).slice(0, 1024)
2307
- });
2308
- throw err;
2309
- }
2310
- },
2311
- async createWorktree({ url, ref, targetPath }) {
2312
- const mirror = mirrorDir(url);
2313
- if (!existsSync(join(mirror, "HEAD"))) throw new GitMirrorError(`Cannot create worktree — no mirror exists for "${url}"`);
2314
- const absTarget = resolve(targetPath);
2315
- if (existsSync(absTarget) && !isHubManagedWorktree(absTarget)) {
2316
- log("worktreeCreateConflict", {
2317
- gitUrl: url,
2318
- targetPath: absTarget,
2319
- occupantKind: classifyOccupant(absTarget)
2320
- });
2321
- throw new GitMirrorWorktreeConflictError(`Worktree target "${absTarget}" is already occupied by ${classifyOccupant(absTarget)} — aborting (D13)`);
2322
- }
2323
- mkdirSync(dirname(absTarget), { recursive: true });
2324
- const args = [
2325
- "worktree",
2326
- "add",
2327
- "--detach",
2328
- absTarget
2329
- ];
2330
- if (ref) args.push(ref);
2331
- await git(args, mirror, cloneTimeoutMs);
2332
- return {
2333
- worktreePath: absTarget,
2334
- headCommit: (await git(["rev-parse", "HEAD"], absTarget, 3e4)).stdout.trim()
2335
- };
2336
- },
2337
- async removeWorktree(path) {
2338
- const absTarget = resolve(path);
2339
- if (!existsSync(absTarget)) return;
2340
- if (!existsSync(mirrorsRoot)) return;
2341
- let removed = false;
2342
- for (const entry of readdirSync(mirrorsRoot)) {
2343
- const mirror = join(mirrorsRoot, entry);
2344
- if (!isBareRepo(mirror)) continue;
2345
- try {
2346
- await git([
2347
- "worktree",
2348
- "remove",
2349
- "--force",
2350
- absTarget
2351
- ], mirror, 3e4);
2352
- removed = true;
2353
- break;
2354
- } catch {}
2355
- }
2356
- if (!removed && existsSync(absTarget)) rmSync(absTarget, {
2357
- recursive: true,
2358
- force: true
2359
- });
2360
- },
2361
- async gcMirrors(stillReferencedUrls) {
2362
- if (!existsSync(mirrorsRoot)) return { removed: [] };
2363
- const wantedHashes = new Set([...stillReferencedUrls].map(hashUrl));
2364
- const removed = [];
2365
- for (const entry of readdirSync(mirrorsRoot)) {
2366
- if (wantedHashes.has(entry)) continue;
2367
- const path = join(mirrorsRoot, entry);
2368
- if (!isBareRepo(path)) continue;
2369
- rmSync(path, {
2370
- recursive: true,
2371
- force: true
2372
- });
2373
- removed.push(entry);
2374
- }
2375
- return { removed };
2376
- }
2377
- };
2378
- }
2379
- function isBareRepo(p) {
2380
- return existsSync(join(p, "HEAD")) && existsSync(join(p, "objects"));
2381
- }
2382
- function isHubManagedWorktree(p) {
2383
- const gitMarker = join(p, ".git");
2384
- if (!existsSync(gitMarker)) return false;
2385
- try {
2386
- return statSync(gitMarker).isFile();
2387
- } catch {
2388
- return false;
2389
- }
2390
- }
2391
- function classifyOccupant(p) {
2392
- try {
2393
- const stat = statSync(p);
2394
- if (stat.isSymbolicLink()) return "symlink";
2395
- if (stat.isDirectory()) {
2396
- if (existsSync(join(p, ".git"))) return "git-repo";
2397
- return "directory";
2398
- }
2399
- if (stat.isFile()) return "file";
2400
- return "other";
2401
- } catch {
2402
- return "unknown";
2403
- }
2404
- }
2405
- var GitMirrorError = class extends Error {
2406
- constructor(message) {
2407
- super(message);
2408
- this.name = "GitMirrorError";
2409
- }
2410
- };
2411
- var GitMirrorTimeoutError = class extends GitMirrorError {
2412
- constructor(message) {
2413
- super(message);
2414
- this.name = "GitMirrorTimeoutError";
2415
- }
2416
- };
2417
- var GitMirrorWorktreeConflictError = class extends GitMirrorError {
2418
- constructor(message) {
2419
- super(message);
2420
- this.name = "GitMirrorWorktreeConflictError";
2421
- }
2422
- };
2423
2657
  /**
2424
2658
  * Deduplicator — bounded set of recently seen IDs.
2425
2659
  *
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-CRDR6NwE.mjs";
2
- import { A as stopPostgres, C as checkServerReachable, D as status, E as blank, F as SdkError, M as createOwner, N as hasUser, O as ensurePostgres, P as FirstTreeHubSDK, S as checkServerHealth, T as printResults, _ as checkClientConfig, a as uninstallClientService, b as checkNodeVersion, c as promptAddAgent, f as onboardCheck, g as checkAgentConfigs, h as runMigrations, i as resolveCliInvocation, j as ClientRuntime, k as isDockerAvailable, l as promptMissingFields, n as installClientService, o as startServer, p as onboardCreate, r as isServiceSupported, s as isInteractive, t as getClientServiceStatus, u as formatCheckReport, v as checkDatabase, w as checkWebSocket, x as checkServerConfig, y as checkDocker } from "./core-C05B8FzH.mjs";
2
+ import { A as stopPostgres, C as checkServerReachable, D as status, E as blank, F as SdkError, M as createOwner, N as hasUser, O as ensurePostgres, P as FirstTreeHubSDK, S as checkServerHealth, T as printResults, _ as checkClientConfig, a as uninstallClientService, b as checkNodeVersion, c as promptAddAgent, f as onboardCheck, g as checkAgentConfigs, h as runMigrations, i as resolveCliInvocation, j as ClientRuntime, k as isDockerAvailable, l as promptMissingFields, n as installClientService, o as startServer, p as onboardCreate, r as isServiceSupported, s as isInteractive, t as getClientServiceStatus, u as formatCheckReport, v as checkDatabase, w as checkWebSocket, x as checkServerConfig, y as checkDocker } from "./core-6-paFwyo.mjs";
3
3
  import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-CJ08ntOD.mjs";
4
4
  export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, runMigrations, startServer, status, stopPostgres, uninstallClientService };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-team-foundation/first-tree-hub",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "description": "First Tree Hub — unified CLI for server, client, and agent management",
6
6
  "exports": {