@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.
- package/dist/cli/index.mjs +1 -1
- package/dist/{core-C05B8FzH.mjs → core-6-paFwyo.mjs} +464 -230
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -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-
|
|
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
|
-
/**
|
|
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(
|
|
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(
|
|
1977
|
-
|
|
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
|
|
1985
|
-
if (!
|
|
2438
|
+
const entry = ownedWorktrees.pop();
|
|
2439
|
+
if (!entry) continue;
|
|
1986
2440
|
try {
|
|
1987
|
-
await gitMirrorManager.removeWorktree(
|
|
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-
|
|
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 };
|