@agent-team-foundation/first-tree-hub 0.7.0 → 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 +56 -3
- package/dist/{core-4nvleGlC.mjs → core-6-paFwyo.mjs} +846 -240
- package/dist/index.mjs +2 -2
- 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 SdkError,
|
|
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";
|
|
@@ -798,7 +798,7 @@ async function authenticateInteractive(url) {
|
|
|
798
798
|
return await loginRes.json();
|
|
799
799
|
}
|
|
800
800
|
function registerConnectCommand(parent) {
|
|
801
|
-
parent.command("connect <server-url>").description("Connect to a Hub server — configure, authenticate, and
|
|
801
|
+
parent.command("connect <server-url>").description("Connect to a Hub server — configure, authenticate, and install the background service").option("--token <token>", "Connect token (from Hub web console) — skips interactive login").option("--no-service", "Skip background service install (runs inline until Ctrl+C)").action(async (serverUrl, options) => {
|
|
802
802
|
try {
|
|
803
803
|
const url = serverUrl.replace(/\/+$/, "");
|
|
804
804
|
setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
|
|
@@ -814,12 +814,23 @@ function registerConnectCommand(parent) {
|
|
|
814
814
|
schema: clientConfigSchema,
|
|
815
815
|
role: "client"
|
|
816
816
|
});
|
|
817
|
+
process.stderr.write(` \u2713 Connected as this computer (id: ${config.client.id})\n`);
|
|
818
|
+
if (options.service !== false && isServiceSupported()) {
|
|
819
|
+
const info = installClientService();
|
|
820
|
+
process.stderr.write(` \u2713 Installed as a background service (${info.platform}) — you can close this terminal\n\n`);
|
|
821
|
+
process.stderr.write(` Unit: ${info.unitPath}\n`);
|
|
822
|
+
process.stderr.write(` Logs: ${info.logDir}\n`);
|
|
823
|
+
if (info.state === "active" && info.detail) process.stderr.write(` State: running (${info.detail})\n`);
|
|
824
|
+
process.stderr.write("\n");
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (options.service === false) process.stderr.write(" (--no-service) running inline — Ctrl+C to stop\n");
|
|
828
|
+
else process.stderr.write(` Background service not supported on ${process.platform}; running inline.\n`);
|
|
817
829
|
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
818
830
|
const agents = loadAgents({
|
|
819
831
|
schema: agentConfigSchema,
|
|
820
832
|
agentsDir
|
|
821
833
|
});
|
|
822
|
-
process.stderr.write(`\n Starting client (id: ${config.client.id})...\n`);
|
|
823
834
|
const runtime = new ClientRuntime(config.server.url, config.client.id);
|
|
824
835
|
for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
|
|
825
836
|
await runtime.start();
|
|
@@ -923,6 +934,48 @@ function registerClientCommands(program) {
|
|
|
923
934
|
process.stderr.write(" No agents directory found.\n");
|
|
924
935
|
}
|
|
925
936
|
});
|
|
937
|
+
const service = client.command("service").description("Install/uninstall the background service that keeps this computer online");
|
|
938
|
+
service.command("install").description("Install as a background service — auto-starts on login/boot").action(() => {
|
|
939
|
+
if (!isServiceSupported()) {
|
|
940
|
+
process.stderr.write(` Background service is not supported on ${process.platform}.\n Run \`first-tree-hub client start\` manually to keep the computer online.
|
|
941
|
+
`);
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
const info = installClientService();
|
|
946
|
+
process.stderr.write(`\n \u2713 Installed as a background service (${info.platform}).\n`);
|
|
947
|
+
process.stderr.write(` Unit: ${info.unitPath}\n`);
|
|
948
|
+
process.stderr.write(` Logs: ${info.logDir}\n`);
|
|
949
|
+
if (info.state === "active") process.stderr.write(` State: running${info.detail ? ` (${info.detail})` : ""}\n`);
|
|
950
|
+
else process.stderr.write(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n`);
|
|
951
|
+
process.stderr.write("\n You can close this terminal — the computer stays online.\n");
|
|
952
|
+
} catch (error) {
|
|
953
|
+
fail("SERVICE_INSTALL_ERROR", error instanceof Error ? error.message : String(error));
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
service.command("status").description("Show background service state").action(() => {
|
|
957
|
+
const info = getClientServiceStatus();
|
|
958
|
+
if (info.platform === "unsupported") {
|
|
959
|
+
process.stderr.write(` Not supported on ${process.platform}.\n`);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
process.stderr.write(`\n ${info.platform}: ${info.label}\n`);
|
|
963
|
+
process.stderr.write(` Unit: ${info.unitPath}\n`);
|
|
964
|
+
process.stderr.write(` Logs: ${info.logDir}\n`);
|
|
965
|
+
process.stderr.write(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n\n`);
|
|
966
|
+
});
|
|
967
|
+
service.command("uninstall").description("Stop and remove the background service").action(() => {
|
|
968
|
+
if (!isServiceSupported()) {
|
|
969
|
+
process.stderr.write(` Not supported on ${process.platform}.\n`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
const info = uninstallClientService();
|
|
974
|
+
process.stderr.write(`\n \u2713 Uninstalled background service (${info.platform}).\n\n`);
|
|
975
|
+
} catch (error) {
|
|
976
|
+
fail("SERVICE_UNINSTALL_ERROR", error instanceof Error ? error.message : String(error));
|
|
977
|
+
}
|
|
978
|
+
});
|
|
926
979
|
client.command("hub-list").description("List clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
|
|
927
980
|
try {
|
|
928
981
|
const serverUrl = resolveServerUrl(options.server);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { C as setConfigValue, S as serverConfigSchema, _ as loadAgents, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, x as resolveConfigReadonly } from "./bootstrap-CRDR6NwE.mjs";
|
|
2
2
|
import { $ as updateAgentSchema, A as createOrganizationSchema, B as paginationQuerySchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as isRedactedEnvValue, G as sendToAgentSchema, H as runtimeStateMessageSchema, I as linkTaskChatSchema, J as sessionEventSchema$1, K as sessionCompletionMessageSchema, L as loginSchema, M as delegateFeishuUserSchema, N as dryRunAgentRuntimeConfigSchema, O as createChatSchema, P as inboxPollQuerySchema, Q as updateAgentRuntimeConfigSchema, R as messageSourceSchema$1, S as agentTypeSchema$1, T as createAdapterConfigSchema, U as selfServiceFeishuBotSchema, V as refreshTokenSchema, W as sendMessageSchema, X as taskListQuerySchema, Y as sessionStateMessageSchema, Z as updateAdapterConfigSchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateMemberSchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, it as wsAuthFrameSchema, j as createTaskSchema, k as createMemberSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as updateSystemConfigSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionEventMessageSchema, rt as updateTaskStatusSchema, s as AGENT_STATUSES, tt as updateOrganizationSchema, u as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, v as adminCreateTaskSchema, w as connectTokenExchangeSchema, x as agentRuntimeConfigPayloadSchema$1, y as adminUpdateTaskSchema, z as notificationQuerySchema } from "./feishu-CJ08ntOD.mjs";
|
|
3
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { ZodError, z } from "zod";
|
|
6
6
|
import "yaml";
|
|
7
7
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
8
|
-
import { homedir, hostname, platform } from "node:os";
|
|
8
|
+
import { homedir, hostname, platform, userInfo } from "node:os";
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
10
|
import WebSocket from "ws";
|
|
11
11
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -1106,7 +1106,6 @@ var ClientConnection = class extends EventEmitter {
|
|
|
1106
1106
|
return;
|
|
1107
1107
|
}
|
|
1108
1108
|
if (type === "auth:rejected" || type === "auth:expired") {
|
|
1109
|
-
this.registered = false;
|
|
1110
1109
|
if (type === "auth:expired") this.emit("auth:expired");
|
|
1111
1110
|
this.ws?.close(4401, type);
|
|
1112
1111
|
return;
|
|
@@ -1494,6 +1493,451 @@ echo "long message body" | first-tree-hub agent send <agentId>
|
|
|
1494
1493
|
For content with quotes, \`$\`, backticks, or newlines, prefer stdin to avoid shell escaping issues.
|
|
1495
1494
|
`;
|
|
1496
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
|
+
};
|
|
1497
1941
|
/**
|
|
1498
1942
|
* InputController — push-based async iterable bridge.
|
|
1499
1943
|
*
|
|
@@ -1742,7 +2186,7 @@ const createClaudeCodeHandler = (config) => {
|
|
|
1742
2186
|
let appliedConfigVersion = 0;
|
|
1743
2187
|
let appliedModel = "";
|
|
1744
2188
|
let appliedPayload = null;
|
|
1745
|
-
/**
|
|
2189
|
+
/** Worktrees materialised for this session — each entry removed on shutdown. */
|
|
1746
2190
|
const ownedWorktrees = [];
|
|
1747
2191
|
function toSDKUserMessage(message, sessionId) {
|
|
1748
2192
|
const rawContent = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
@@ -1966,28 +2410,37 @@ const createClaudeCodeHandler = (config) => {
|
|
|
1966
2410
|
await gitMirrorManager.fetchMirror(repo.url);
|
|
1967
2411
|
if (existsSync(targetPath) && isHubWorktreeMarker(targetPath)) {
|
|
1968
2412
|
sessionCtx.log(`Git: reusing existing worktree at ${localPath}`);
|
|
1969
|
-
ownedWorktrees.push(
|
|
2413
|
+
ownedWorktrees.push({
|
|
2414
|
+
url: repo.url,
|
|
2415
|
+
path: targetPath,
|
|
2416
|
+
branchName: deriveSessionBranchName(sessionCtx.chatId, repo.url)
|
|
2417
|
+
});
|
|
1970
2418
|
continue;
|
|
1971
2419
|
}
|
|
1972
|
-
const { headCommit } = await gitMirrorManager.createWorktree({
|
|
2420
|
+
const { headCommit, branchName } = await gitMirrorManager.createWorktree({
|
|
1973
2421
|
url: repo.url,
|
|
1974
2422
|
ref: repo.ref,
|
|
1975
|
-
targetPath
|
|
2423
|
+
targetPath,
|
|
2424
|
+
sessionKey: sessionCtx.chatId
|
|
1976
2425
|
});
|
|
1977
|
-
ownedWorktrees.push(
|
|
1978
|
-
|
|
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}`);
|
|
1979
2432
|
}
|
|
1980
2433
|
}
|
|
1981
2434
|
/** Tear down all worktrees this session owns; best-effort. */
|
|
1982
2435
|
async function cleanupGitWorktrees(sessionCtx) {
|
|
1983
2436
|
if (!gitMirrorManager) return;
|
|
1984
2437
|
while (ownedWorktrees.length > 0) {
|
|
1985
|
-
const
|
|
1986
|
-
if (!
|
|
2438
|
+
const entry = ownedWorktrees.pop();
|
|
2439
|
+
if (!entry) continue;
|
|
1987
2440
|
try {
|
|
1988
|
-
await gitMirrorManager.removeWorktree(
|
|
2441
|
+
await gitMirrorManager.removeWorktree(entry);
|
|
1989
2442
|
} catch (err) {
|
|
1990
|
-
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)}`);
|
|
1991
2444
|
}
|
|
1992
2445
|
}
|
|
1993
2446
|
}
|
|
@@ -2201,226 +2654,6 @@ function createAgentConfigCache(opts) {
|
|
|
2201
2654
|
}
|
|
2202
2655
|
};
|
|
2203
2656
|
}
|
|
2204
|
-
const DEFAULT_CLONE_TIMEOUT_MS = 300 * 1e3;
|
|
2205
|
-
function hashUrl(url) {
|
|
2206
|
-
return createHash("sha256").update(url).digest("hex").slice(0, 32);
|
|
2207
|
-
}
|
|
2208
|
-
function createGitMirrorManager(opts) {
|
|
2209
|
-
const mirrorsRoot = join(opts.dataDir, "git-mirrors");
|
|
2210
|
-
const cloneTimeoutMs = opts.cloneTimeoutMs ?? Number(process.env.FIRST_TREE_HUB_GIT_CLONE_TIMEOUT_MS ?? DEFAULT_CLONE_TIMEOUT_MS);
|
|
2211
|
-
const log = opts.log ?? (() => {});
|
|
2212
|
-
function mirrorDir(url) {
|
|
2213
|
-
return join(mirrorsRoot, hashUrl(url));
|
|
2214
|
-
}
|
|
2215
|
-
async function git(args, cwd, timeoutMs, env) {
|
|
2216
|
-
const start = Date.now();
|
|
2217
|
-
return await new Promise((resolveExec, rejectExec) => {
|
|
2218
|
-
const proc = spawn("git", args, {
|
|
2219
|
-
cwd: cwd ?? void 0,
|
|
2220
|
-
env: env ?? process.env,
|
|
2221
|
-
stdio: [
|
|
2222
|
-
"ignore",
|
|
2223
|
-
"pipe",
|
|
2224
|
-
"pipe"
|
|
2225
|
-
]
|
|
2226
|
-
});
|
|
2227
|
-
let stdout = "";
|
|
2228
|
-
let stderr = "";
|
|
2229
|
-
proc.stdout.on("data", (d) => {
|
|
2230
|
-
stdout += String(d);
|
|
2231
|
-
});
|
|
2232
|
-
proc.stderr.on("data", (d) => {
|
|
2233
|
-
stderr += String(d);
|
|
2234
|
-
});
|
|
2235
|
-
const timer = setTimeout(() => {
|
|
2236
|
-
proc.kill("SIGKILL");
|
|
2237
|
-
rejectExec(new GitMirrorTimeoutError(`git ${args.join(" ")} timed out after ${timeoutMs}ms`));
|
|
2238
|
-
}, timeoutMs);
|
|
2239
|
-
proc.on("error", (err) => {
|
|
2240
|
-
clearTimeout(timer);
|
|
2241
|
-
rejectExec(err);
|
|
2242
|
-
});
|
|
2243
|
-
proc.on("close", (code) => {
|
|
2244
|
-
clearTimeout(timer);
|
|
2245
|
-
const elapsedMs = Date.now() - start;
|
|
2246
|
-
if (code === 0) resolveExec({
|
|
2247
|
-
stdout,
|
|
2248
|
-
stderr,
|
|
2249
|
-
elapsedMs
|
|
2250
|
-
});
|
|
2251
|
-
else rejectExec(new GitMirrorError(`git ${args.join(" ")} exited with code ${code}: ${stderr.slice(0, 1024)}`));
|
|
2252
|
-
});
|
|
2253
|
-
});
|
|
2254
|
-
}
|
|
2255
|
-
return {
|
|
2256
|
-
get mirrorsRoot() {
|
|
2257
|
-
return mirrorsRoot;
|
|
2258
|
-
},
|
|
2259
|
-
async ensureMirror(url) {
|
|
2260
|
-
mkdirSync(mirrorsRoot, { recursive: true });
|
|
2261
|
-
const path = mirrorDir(url);
|
|
2262
|
-
if (existsSync(join(path, "HEAD"))) return {
|
|
2263
|
-
mirrorPath: path,
|
|
2264
|
-
elapsedMs: 0,
|
|
2265
|
-
cloned: false
|
|
2266
|
-
};
|
|
2267
|
-
try {
|
|
2268
|
-
const { elapsedMs } = await git([
|
|
2269
|
-
"clone",
|
|
2270
|
-
"--mirror",
|
|
2271
|
-
url,
|
|
2272
|
-
path
|
|
2273
|
-
], null, cloneTimeoutMs);
|
|
2274
|
-
log("ensureMirror", {
|
|
2275
|
-
gitUrl: url,
|
|
2276
|
-
elapsedMs,
|
|
2277
|
-
cloned: true
|
|
2278
|
-
});
|
|
2279
|
-
return {
|
|
2280
|
-
mirrorPath: path,
|
|
2281
|
-
elapsedMs,
|
|
2282
|
-
cloned: true
|
|
2283
|
-
};
|
|
2284
|
-
} catch (err) {
|
|
2285
|
-
if (err instanceof GitMirrorTimeoutError) log("mirrorCloneTimeout", {
|
|
2286
|
-
gitUrl: url,
|
|
2287
|
-
timeoutMs: cloneTimeoutMs,
|
|
2288
|
-
elapsedMs: cloneTimeoutMs
|
|
2289
|
-
});
|
|
2290
|
-
if (existsSync(path)) rmSync(path, {
|
|
2291
|
-
recursive: true,
|
|
2292
|
-
force: true
|
|
2293
|
-
});
|
|
2294
|
-
throw err;
|
|
2295
|
-
}
|
|
2296
|
-
},
|
|
2297
|
-
async fetchMirror(url) {
|
|
2298
|
-
const path = mirrorDir(url);
|
|
2299
|
-
if (!existsSync(join(path, "HEAD"))) throw new GitMirrorError(`Cannot fetch — no mirror exists for "${url}"`);
|
|
2300
|
-
try {
|
|
2301
|
-
const { elapsedMs } = await git(["fetch", "--prune"], path, cloneTimeoutMs);
|
|
2302
|
-
return { elapsedMs };
|
|
2303
|
-
} catch (err) {
|
|
2304
|
-
log("mirrorFetchFailed", {
|
|
2305
|
-
gitUrl: url,
|
|
2306
|
-
errorCode: err instanceof GitMirrorError ? "git-failed" : "unknown",
|
|
2307
|
-
stderr: err instanceof Error ? err.message.slice(0, 1024) : String(err).slice(0, 1024)
|
|
2308
|
-
});
|
|
2309
|
-
throw err;
|
|
2310
|
-
}
|
|
2311
|
-
},
|
|
2312
|
-
async createWorktree({ url, ref, targetPath }) {
|
|
2313
|
-
const mirror = mirrorDir(url);
|
|
2314
|
-
if (!existsSync(join(mirror, "HEAD"))) throw new GitMirrorError(`Cannot create worktree — no mirror exists for "${url}"`);
|
|
2315
|
-
const absTarget = resolve(targetPath);
|
|
2316
|
-
if (existsSync(absTarget) && !isHubManagedWorktree(absTarget)) {
|
|
2317
|
-
log("worktreeCreateConflict", {
|
|
2318
|
-
gitUrl: url,
|
|
2319
|
-
targetPath: absTarget,
|
|
2320
|
-
occupantKind: classifyOccupant(absTarget)
|
|
2321
|
-
});
|
|
2322
|
-
throw new GitMirrorWorktreeConflictError(`Worktree target "${absTarget}" is already occupied by ${classifyOccupant(absTarget)} — aborting (D13)`);
|
|
2323
|
-
}
|
|
2324
|
-
mkdirSync(dirname(absTarget), { recursive: true });
|
|
2325
|
-
const args = [
|
|
2326
|
-
"worktree",
|
|
2327
|
-
"add",
|
|
2328
|
-
"--detach",
|
|
2329
|
-
absTarget
|
|
2330
|
-
];
|
|
2331
|
-
if (ref) args.push(ref);
|
|
2332
|
-
await git(args, mirror, cloneTimeoutMs);
|
|
2333
|
-
return {
|
|
2334
|
-
worktreePath: absTarget,
|
|
2335
|
-
headCommit: (await git(["rev-parse", "HEAD"], absTarget, 3e4)).stdout.trim()
|
|
2336
|
-
};
|
|
2337
|
-
},
|
|
2338
|
-
async removeWorktree(path) {
|
|
2339
|
-
const absTarget = resolve(path);
|
|
2340
|
-
if (!existsSync(absTarget)) return;
|
|
2341
|
-
if (!existsSync(mirrorsRoot)) return;
|
|
2342
|
-
let removed = false;
|
|
2343
|
-
for (const entry of readdirSync(mirrorsRoot)) {
|
|
2344
|
-
const mirror = join(mirrorsRoot, entry);
|
|
2345
|
-
if (!isBareRepo(mirror)) continue;
|
|
2346
|
-
try {
|
|
2347
|
-
await git([
|
|
2348
|
-
"worktree",
|
|
2349
|
-
"remove",
|
|
2350
|
-
"--force",
|
|
2351
|
-
absTarget
|
|
2352
|
-
], mirror, 3e4);
|
|
2353
|
-
removed = true;
|
|
2354
|
-
break;
|
|
2355
|
-
} catch {}
|
|
2356
|
-
}
|
|
2357
|
-
if (!removed && existsSync(absTarget)) rmSync(absTarget, {
|
|
2358
|
-
recursive: true,
|
|
2359
|
-
force: true
|
|
2360
|
-
});
|
|
2361
|
-
},
|
|
2362
|
-
async gcMirrors(stillReferencedUrls) {
|
|
2363
|
-
if (!existsSync(mirrorsRoot)) return { removed: [] };
|
|
2364
|
-
const wantedHashes = new Set([...stillReferencedUrls].map(hashUrl));
|
|
2365
|
-
const removed = [];
|
|
2366
|
-
for (const entry of readdirSync(mirrorsRoot)) {
|
|
2367
|
-
if (wantedHashes.has(entry)) continue;
|
|
2368
|
-
const path = join(mirrorsRoot, entry);
|
|
2369
|
-
if (!isBareRepo(path)) continue;
|
|
2370
|
-
rmSync(path, {
|
|
2371
|
-
recursive: true,
|
|
2372
|
-
force: true
|
|
2373
|
-
});
|
|
2374
|
-
removed.push(entry);
|
|
2375
|
-
}
|
|
2376
|
-
return { removed };
|
|
2377
|
-
}
|
|
2378
|
-
};
|
|
2379
|
-
}
|
|
2380
|
-
function isBareRepo(p) {
|
|
2381
|
-
return existsSync(join(p, "HEAD")) && existsSync(join(p, "objects"));
|
|
2382
|
-
}
|
|
2383
|
-
function isHubManagedWorktree(p) {
|
|
2384
|
-
const gitMarker = join(p, ".git");
|
|
2385
|
-
if (!existsSync(gitMarker)) return false;
|
|
2386
|
-
try {
|
|
2387
|
-
return statSync(gitMarker).isFile();
|
|
2388
|
-
} catch {
|
|
2389
|
-
return false;
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
function classifyOccupant(p) {
|
|
2393
|
-
try {
|
|
2394
|
-
const stat = statSync(p);
|
|
2395
|
-
if (stat.isSymbolicLink()) return "symlink";
|
|
2396
|
-
if (stat.isDirectory()) {
|
|
2397
|
-
if (existsSync(join(p, ".git"))) return "git-repo";
|
|
2398
|
-
return "directory";
|
|
2399
|
-
}
|
|
2400
|
-
if (stat.isFile()) return "file";
|
|
2401
|
-
return "other";
|
|
2402
|
-
} catch {
|
|
2403
|
-
return "unknown";
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
var GitMirrorError = class extends Error {
|
|
2407
|
-
constructor(message) {
|
|
2408
|
-
super(message);
|
|
2409
|
-
this.name = "GitMirrorError";
|
|
2410
|
-
}
|
|
2411
|
-
};
|
|
2412
|
-
var GitMirrorTimeoutError = class extends GitMirrorError {
|
|
2413
|
-
constructor(message) {
|
|
2414
|
-
super(message);
|
|
2415
|
-
this.name = "GitMirrorTimeoutError";
|
|
2416
|
-
}
|
|
2417
|
-
};
|
|
2418
|
-
var GitMirrorWorktreeConflictError = class extends GitMirrorError {
|
|
2419
|
-
constructor(message) {
|
|
2420
|
-
super(message);
|
|
2421
|
-
this.name = "GitMirrorWorktreeConflictError";
|
|
2422
|
-
}
|
|
2423
|
-
};
|
|
2424
2657
|
/**
|
|
2425
2658
|
* Deduplicator — bounded set of recently seen IDs.
|
|
2426
2659
|
*
|
|
@@ -4114,7 +4347,7 @@ function setNestedByDot(obj, dotPath, value) {
|
|
|
4114
4347
|
if (lastKey !== void 0) current[lastKey] = value;
|
|
4115
4348
|
}
|
|
4116
4349
|
//#endregion
|
|
4117
|
-
//#region ../server/dist/app-
|
|
4350
|
+
//#region ../server/dist/app-CWKBBGod.mjs
|
|
4118
4351
|
var __defProp = Object.defineProperty;
|
|
4119
4352
|
var __exportAll = (all, no_symbols) => {
|
|
4120
4353
|
let target = {};
|
|
@@ -4649,8 +4882,17 @@ function parseId$1(raw) {
|
|
|
4649
4882
|
return id;
|
|
4650
4883
|
}
|
|
4651
4884
|
async function adminAdapterMappingRoutes(app) {
|
|
4652
|
-
app.get("/", async () => {
|
|
4653
|
-
|
|
4885
|
+
app.get("/", async (request) => {
|
|
4886
|
+
const scope = memberScope(request);
|
|
4887
|
+
return (await app.db.select({
|
|
4888
|
+
id: adapterAgentMappings.id,
|
|
4889
|
+
platform: adapterAgentMappings.platform,
|
|
4890
|
+
externalUserId: adapterAgentMappings.externalUserId,
|
|
4891
|
+
agentId: adapterAgentMappings.agentId,
|
|
4892
|
+
boundVia: adapterAgentMappings.boundVia,
|
|
4893
|
+
displayName: adapterAgentMappings.displayName,
|
|
4894
|
+
createdAt: adapterAgentMappings.createdAt
|
|
4895
|
+
}).from(adapterAgentMappings).innerJoin(agents, eq(agents.uuid, adapterAgentMappings.agentId)).where(eq(agents.organizationId, scope.organizationId)).orderBy(desc(adapterAgentMappings.createdAt))).map((r) => ({
|
|
4654
4896
|
id: r.id,
|
|
4655
4897
|
platform: r.platform,
|
|
4656
4898
|
externalUserId: r.externalUserId,
|
|
@@ -5077,8 +5319,23 @@ async function createAgent(db, data) {
|
|
|
5077
5319
|
const name = data.name ?? null;
|
|
5078
5320
|
if (name?.startsWith(RESERVED_AGENT_NAME_PREFIX)) throw new BadRequestError(`Agent name "${name}" is reserved — names starting with "${RESERVED_AGENT_NAME_PREFIX}" are Hub-internal`);
|
|
5079
5321
|
const inboxId = `inbox_${uuid}`;
|
|
5080
|
-
|
|
5081
|
-
|
|
5322
|
+
let orgId;
|
|
5323
|
+
let managerId;
|
|
5324
|
+
if (data.managerId && data.organizationId) {
|
|
5325
|
+
orgId = data.organizationId;
|
|
5326
|
+
managerId = data.managerId;
|
|
5327
|
+
} else if (data.managerId) {
|
|
5328
|
+
const [manager] = await db.select({
|
|
5329
|
+
id: members.id,
|
|
5330
|
+
organizationId: members.organizationId
|
|
5331
|
+
}).from(members).where(eq(members.id, data.managerId)).limit(1);
|
|
5332
|
+
if (!manager) throw new BadRequestError(`Manager "${data.managerId}" not found`);
|
|
5333
|
+
orgId = manager.organizationId;
|
|
5334
|
+
managerId = manager.id;
|
|
5335
|
+
} else {
|
|
5336
|
+
orgId = data.organizationId ?? await resolveDefaultOrgId(db);
|
|
5337
|
+
managerId = await resolveFallbackManagerId(db, orgId);
|
|
5338
|
+
}
|
|
5082
5339
|
const clientId = await resolveAgentClient(db, {
|
|
5083
5340
|
clientId: data.clientId,
|
|
5084
5341
|
managerId,
|
|
@@ -11044,4 +11301,353 @@ function resolveWebDist() {
|
|
|
11044
11301
|
} catch {}
|
|
11045
11302
|
}
|
|
11046
11303
|
//#endregion
|
|
11047
|
-
|
|
11304
|
+
//#region src/core/service-install.ts
|
|
11305
|
+
const LAUNCHD_LABEL = "dev.first-tree-hub.client";
|
|
11306
|
+
const SYSTEMD_UNIT = "first-tree-hub-client.service";
|
|
11307
|
+
const LOG_DIR = join(DEFAULT_HOME_DIR$1, "logs");
|
|
11308
|
+
function whichBin(name) {
|
|
11309
|
+
try {
|
|
11310
|
+
return execFileSync(process.platform === "win32" ? "where" : "which", [name], {
|
|
11311
|
+
encoding: "utf-8",
|
|
11312
|
+
timeout: 3e3
|
|
11313
|
+
}).split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0] ?? null;
|
|
11314
|
+
} catch {
|
|
11315
|
+
return null;
|
|
11316
|
+
}
|
|
11317
|
+
}
|
|
11318
|
+
/**
|
|
11319
|
+
* Resolve how the service should launch the CLI.
|
|
11320
|
+
*
|
|
11321
|
+
* Prefers the installed `first-tree-hub` bin on PATH (usually a shim under
|
|
11322
|
+
* /usr/local/bin or ~/.npm-global/bin). Falls back to invoking the current
|
|
11323
|
+
* Node interpreter against the running script (handles `pnpm dev`, tsx, and
|
|
11324
|
+
* dev-only global installs).
|
|
11325
|
+
*/
|
|
11326
|
+
function resolveCliInvocation() {
|
|
11327
|
+
const bin = whichBin("first-tree-hub");
|
|
11328
|
+
if (bin && isAbsolute(bin)) try {
|
|
11329
|
+
return {
|
|
11330
|
+
kind: "bin",
|
|
11331
|
+
program: realpathSync(bin)
|
|
11332
|
+
};
|
|
11333
|
+
} catch {
|
|
11334
|
+
return {
|
|
11335
|
+
kind: "bin",
|
|
11336
|
+
program: bin
|
|
11337
|
+
};
|
|
11338
|
+
}
|
|
11339
|
+
const script = process.argv[1];
|
|
11340
|
+
if (!script) throw new Error("Cannot resolve CLI entry point (process.argv[1] is empty).");
|
|
11341
|
+
const scriptAbs = isAbsolute(script) ? script : join(process.cwd(), script);
|
|
11342
|
+
return {
|
|
11343
|
+
kind: "node",
|
|
11344
|
+
program: process.execPath,
|
|
11345
|
+
args: [scriptAbs]
|
|
11346
|
+
};
|
|
11347
|
+
}
|
|
11348
|
+
function ensureLogDir() {
|
|
11349
|
+
mkdirSync(LOG_DIR, {
|
|
11350
|
+
recursive: true,
|
|
11351
|
+
mode: 448
|
|
11352
|
+
});
|
|
11353
|
+
}
|
|
11354
|
+
function launchdPlistPath() {
|
|
11355
|
+
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
11356
|
+
}
|
|
11357
|
+
function renderPlist(invocation) {
|
|
11358
|
+
const argsXml = (invocation.kind === "bin" ? [
|
|
11359
|
+
invocation.program,
|
|
11360
|
+
"client",
|
|
11361
|
+
"start",
|
|
11362
|
+
"--no-interactive"
|
|
11363
|
+
] : [
|
|
11364
|
+
invocation.program,
|
|
11365
|
+
...invocation.args,
|
|
11366
|
+
"client",
|
|
11367
|
+
"start",
|
|
11368
|
+
"--no-interactive"
|
|
11369
|
+
]).map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
11370
|
+
const outLog = join(LOG_DIR, "client.out.log");
|
|
11371
|
+
const errLog = join(LOG_DIR, "client.err.log");
|
|
11372
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
11373
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTD/PropertyList-1.0.dtd">
|
|
11374
|
+
<plist version="1.0">
|
|
11375
|
+
<dict>
|
|
11376
|
+
<key>Label</key>
|
|
11377
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
11378
|
+
<key>ProgramArguments</key>
|
|
11379
|
+
<array>
|
|
11380
|
+
${argsXml}
|
|
11381
|
+
</array>
|
|
11382
|
+
<key>EnvironmentVariables</key>
|
|
11383
|
+
<dict>
|
|
11384
|
+
<key>PATH</key>
|
|
11385
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
11386
|
+
</dict>
|
|
11387
|
+
<key>RunAtLoad</key>
|
|
11388
|
+
<true/>
|
|
11389
|
+
<key>KeepAlive</key>
|
|
11390
|
+
<dict>
|
|
11391
|
+
<key>SuccessfulExit</key>
|
|
11392
|
+
<false/>
|
|
11393
|
+
</dict>
|
|
11394
|
+
<key>ThrottleInterval</key>
|
|
11395
|
+
<integer>10</integer>
|
|
11396
|
+
<key>StandardOutPath</key>
|
|
11397
|
+
<string>${escapeXml(outLog)}</string>
|
|
11398
|
+
<key>StandardErrorPath</key>
|
|
11399
|
+
<string>${escapeXml(errLog)}</string>
|
|
11400
|
+
</dict>
|
|
11401
|
+
</plist>
|
|
11402
|
+
`;
|
|
11403
|
+
}
|
|
11404
|
+
function escapeXml(value) {
|
|
11405
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
11406
|
+
}
|
|
11407
|
+
function launchctlDomainTarget() {
|
|
11408
|
+
return `gui/${userInfo().uid}`;
|
|
11409
|
+
}
|
|
11410
|
+
function launchdState() {
|
|
11411
|
+
if (!existsSync(launchdPlistPath())) return { state: "not-installed" };
|
|
11412
|
+
try {
|
|
11413
|
+
const out = execFileSync("launchctl", ["print", `${launchctlDomainTarget()}/${LAUNCHD_LABEL}`], {
|
|
11414
|
+
encoding: "utf-8",
|
|
11415
|
+
timeout: 5e3
|
|
11416
|
+
});
|
|
11417
|
+
const stateLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("state ="));
|
|
11418
|
+
const pidLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("pid ="));
|
|
11419
|
+
if (stateLine?.includes("running")) {
|
|
11420
|
+
const pid = pidLine?.split("=")[1]?.trim();
|
|
11421
|
+
return {
|
|
11422
|
+
state: "active",
|
|
11423
|
+
detail: pid ? `pid ${pid}` : "running"
|
|
11424
|
+
};
|
|
11425
|
+
}
|
|
11426
|
+
return {
|
|
11427
|
+
state: "inactive",
|
|
11428
|
+
detail: stateLine?.trim() ?? "loaded"
|
|
11429
|
+
};
|
|
11430
|
+
} catch {
|
|
11431
|
+
return {
|
|
11432
|
+
state: "inactive",
|
|
11433
|
+
detail: "plist present but not loaded"
|
|
11434
|
+
};
|
|
11435
|
+
}
|
|
11436
|
+
}
|
|
11437
|
+
function installLaunchd() {
|
|
11438
|
+
const invocation = resolveCliInvocation();
|
|
11439
|
+
ensureLogDir();
|
|
11440
|
+
const plistPath = launchdPlistPath();
|
|
11441
|
+
mkdirSync(dirname(plistPath), { recursive: true });
|
|
11442
|
+
writeFileSync(plistPath, renderPlist(invocation), { mode: 420 });
|
|
11443
|
+
const target = launchctlDomainTarget();
|
|
11444
|
+
try {
|
|
11445
|
+
execFileSync("launchctl", ["bootout", `${target}/${LAUNCHD_LABEL}`], {
|
|
11446
|
+
stdio: "ignore",
|
|
11447
|
+
timeout: 5e3
|
|
11448
|
+
});
|
|
11449
|
+
} catch {}
|
|
11450
|
+
execFileSync("launchctl", [
|
|
11451
|
+
"bootstrap",
|
|
11452
|
+
target,
|
|
11453
|
+
plistPath
|
|
11454
|
+
], {
|
|
11455
|
+
stdio: "ignore",
|
|
11456
|
+
timeout: 5e3
|
|
11457
|
+
});
|
|
11458
|
+
execFileSync("launchctl", ["enable", `${target}/${LAUNCHD_LABEL}`], {
|
|
11459
|
+
stdio: "ignore",
|
|
11460
|
+
timeout: 5e3
|
|
11461
|
+
});
|
|
11462
|
+
const { state, detail } = launchdState();
|
|
11463
|
+
return {
|
|
11464
|
+
platform: "launchd",
|
|
11465
|
+
label: LAUNCHD_LABEL,
|
|
11466
|
+
unitPath: plistPath,
|
|
11467
|
+
logDir: LOG_DIR,
|
|
11468
|
+
state,
|
|
11469
|
+
detail
|
|
11470
|
+
};
|
|
11471
|
+
}
|
|
11472
|
+
function uninstallLaunchd() {
|
|
11473
|
+
const plistPath = launchdPlistPath();
|
|
11474
|
+
const target = launchctlDomainTarget();
|
|
11475
|
+
try {
|
|
11476
|
+
execFileSync("launchctl", ["bootout", `${target}/${LAUNCHD_LABEL}`], {
|
|
11477
|
+
stdio: "ignore",
|
|
11478
|
+
timeout: 5e3
|
|
11479
|
+
});
|
|
11480
|
+
} catch {}
|
|
11481
|
+
if (existsSync(plistPath)) rmSync(plistPath);
|
|
11482
|
+
return {
|
|
11483
|
+
platform: "launchd",
|
|
11484
|
+
label: LAUNCHD_LABEL,
|
|
11485
|
+
unitPath: plistPath,
|
|
11486
|
+
logDir: LOG_DIR,
|
|
11487
|
+
state: "not-installed"
|
|
11488
|
+
};
|
|
11489
|
+
}
|
|
11490
|
+
function systemdUnitPath() {
|
|
11491
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "systemd", "user", SYSTEMD_UNIT);
|
|
11492
|
+
}
|
|
11493
|
+
function renderSystemdUnit(invocation) {
|
|
11494
|
+
return `[Unit]
|
|
11495
|
+
Description=First Tree Hub Client
|
|
11496
|
+
After=network-online.target
|
|
11497
|
+
Wants=network-online.target
|
|
11498
|
+
|
|
11499
|
+
[Service]
|
|
11500
|
+
Type=simple
|
|
11501
|
+
ExecStart=${invocation.kind === "bin" ? `${shellQuote(invocation.program)} client start --no-interactive` : `${shellQuote(invocation.program)} ${invocation.args.map(shellQuote).join(" ")} client start --no-interactive`}
|
|
11502
|
+
Restart=always
|
|
11503
|
+
RestartSec=10
|
|
11504
|
+
StandardOutput=append:${join(LOG_DIR, "client.out.log")}
|
|
11505
|
+
StandardError=append:${join(LOG_DIR, "client.err.log")}
|
|
11506
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
11507
|
+
|
|
11508
|
+
[Install]
|
|
11509
|
+
WantedBy=default.target
|
|
11510
|
+
`;
|
|
11511
|
+
}
|
|
11512
|
+
function shellQuote(value) {
|
|
11513
|
+
if (/^[A-Za-z0-9_\-./:=]+$/.test(value)) return value;
|
|
11514
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
11515
|
+
}
|
|
11516
|
+
function systemdState() {
|
|
11517
|
+
if (!existsSync(systemdUnitPath())) return { state: "not-installed" };
|
|
11518
|
+
try {
|
|
11519
|
+
const out = execFileSync("systemctl", [
|
|
11520
|
+
"--user",
|
|
11521
|
+
"is-active",
|
|
11522
|
+
SYSTEMD_UNIT
|
|
11523
|
+
], {
|
|
11524
|
+
encoding: "utf-8",
|
|
11525
|
+
timeout: 5e3
|
|
11526
|
+
}).trim();
|
|
11527
|
+
if (out === "active") return {
|
|
11528
|
+
state: "active",
|
|
11529
|
+
detail: "running"
|
|
11530
|
+
};
|
|
11531
|
+
return {
|
|
11532
|
+
state: "inactive",
|
|
11533
|
+
detail: out
|
|
11534
|
+
};
|
|
11535
|
+
} catch (err) {
|
|
11536
|
+
return {
|
|
11537
|
+
state: "inactive",
|
|
11538
|
+
detail: (typeof err.stdout === "string" ? (err.stdout ?? "").trim() : "") || "unit present but not active"
|
|
11539
|
+
};
|
|
11540
|
+
}
|
|
11541
|
+
}
|
|
11542
|
+
function installSystemd() {
|
|
11543
|
+
const invocation = resolveCliInvocation();
|
|
11544
|
+
ensureLogDir();
|
|
11545
|
+
const unitPath = systemdUnitPath();
|
|
11546
|
+
mkdirSync(dirname(unitPath), { recursive: true });
|
|
11547
|
+
writeFileSync(unitPath, renderSystemdUnit(invocation), { mode: 420 });
|
|
11548
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], {
|
|
11549
|
+
stdio: "ignore",
|
|
11550
|
+
timeout: 5e3
|
|
11551
|
+
});
|
|
11552
|
+
execFileSync("systemctl", [
|
|
11553
|
+
"--user",
|
|
11554
|
+
"enable",
|
|
11555
|
+
"--now",
|
|
11556
|
+
SYSTEMD_UNIT
|
|
11557
|
+
], {
|
|
11558
|
+
stdio: "ignore",
|
|
11559
|
+
timeout: 1e4
|
|
11560
|
+
});
|
|
11561
|
+
const { state, detail } = systemdState();
|
|
11562
|
+
return {
|
|
11563
|
+
platform: "systemd",
|
|
11564
|
+
label: SYSTEMD_UNIT,
|
|
11565
|
+
unitPath,
|
|
11566
|
+
logDir: LOG_DIR,
|
|
11567
|
+
state,
|
|
11568
|
+
detail
|
|
11569
|
+
};
|
|
11570
|
+
}
|
|
11571
|
+
function uninstallSystemd() {
|
|
11572
|
+
const unitPath = systemdUnitPath();
|
|
11573
|
+
try {
|
|
11574
|
+
execFileSync("systemctl", [
|
|
11575
|
+
"--user",
|
|
11576
|
+
"disable",
|
|
11577
|
+
"--now",
|
|
11578
|
+
SYSTEMD_UNIT
|
|
11579
|
+
], {
|
|
11580
|
+
stdio: "ignore",
|
|
11581
|
+
timeout: 1e4
|
|
11582
|
+
});
|
|
11583
|
+
} catch {}
|
|
11584
|
+
if (existsSync(unitPath)) rmSync(unitPath);
|
|
11585
|
+
try {
|
|
11586
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], {
|
|
11587
|
+
stdio: "ignore",
|
|
11588
|
+
timeout: 5e3
|
|
11589
|
+
});
|
|
11590
|
+
} catch {}
|
|
11591
|
+
return {
|
|
11592
|
+
platform: "systemd",
|
|
11593
|
+
label: SYSTEMD_UNIT,
|
|
11594
|
+
unitPath,
|
|
11595
|
+
logDir: LOG_DIR,
|
|
11596
|
+
state: "not-installed"
|
|
11597
|
+
};
|
|
11598
|
+
}
|
|
11599
|
+
/** Is background-service install supported on the current platform? */
|
|
11600
|
+
function isServiceSupported() {
|
|
11601
|
+
return process.platform === "darwin" || process.platform === "linux";
|
|
11602
|
+
}
|
|
11603
|
+
/**
|
|
11604
|
+
* Install the background service for the current platform.
|
|
11605
|
+
*
|
|
11606
|
+
* @throws {Error} if the platform is not supported or the service manager fails.
|
|
11607
|
+
*/
|
|
11608
|
+
function installClientService() {
|
|
11609
|
+
if (process.platform === "darwin") return installLaunchd();
|
|
11610
|
+
if (process.platform === "linux") return installSystemd();
|
|
11611
|
+
throw new Error(`Background service install is not supported on ${process.platform}. Run \`first-tree-hub client start\` manually to keep the computer online.`);
|
|
11612
|
+
}
|
|
11613
|
+
/** Report the current service state without modifying anything. */
|
|
11614
|
+
function getClientServiceStatus() {
|
|
11615
|
+
if (process.platform === "darwin") {
|
|
11616
|
+
const { state, detail } = launchdState();
|
|
11617
|
+
return {
|
|
11618
|
+
platform: "launchd",
|
|
11619
|
+
label: LAUNCHD_LABEL,
|
|
11620
|
+
unitPath: launchdPlistPath(),
|
|
11621
|
+
logDir: LOG_DIR,
|
|
11622
|
+
state,
|
|
11623
|
+
detail
|
|
11624
|
+
};
|
|
11625
|
+
}
|
|
11626
|
+
if (process.platform === "linux") {
|
|
11627
|
+
const { state, detail } = systemdState();
|
|
11628
|
+
return {
|
|
11629
|
+
platform: "systemd",
|
|
11630
|
+
label: SYSTEMD_UNIT,
|
|
11631
|
+
unitPath: systemdUnitPath(),
|
|
11632
|
+
logDir: LOG_DIR,
|
|
11633
|
+
state,
|
|
11634
|
+
detail
|
|
11635
|
+
};
|
|
11636
|
+
}
|
|
11637
|
+
return {
|
|
11638
|
+
platform: "unsupported",
|
|
11639
|
+
label: "",
|
|
11640
|
+
unitPath: "",
|
|
11641
|
+
logDir: LOG_DIR,
|
|
11642
|
+
state: "not-installed",
|
|
11643
|
+
detail: `platform ${process.platform} not supported`
|
|
11644
|
+
};
|
|
11645
|
+
}
|
|
11646
|
+
/** Uninstall the background service. No-op if not installed. */
|
|
11647
|
+
function uninstallClientService() {
|
|
11648
|
+
if (process.platform === "darwin") return uninstallLaunchd();
|
|
11649
|
+
if (process.platform === "linux") return uninstallSystemd();
|
|
11650
|
+
return getClientServiceStatus();
|
|
11651
|
+
}
|
|
11652
|
+
//#endregion
|
|
11653
|
+
export { stopPostgres as A, checkServerReachable as C, status as D, blank as E, SdkError as F, SessionRegistry as I, cleanWorkspaces as L, createOwner as M, hasUser as N, ensurePostgres as O, FirstTreeHubSDK as P, checkServerHealth as S, printResults as T, checkClientConfig as _, uninstallClientService as a, checkNodeVersion as b, promptAddAgent as c, loadOnboardState as d, onboardCheck as f, checkAgentConfigs as g, runMigrations as h, resolveCliInvocation as i, ClientRuntime as j, isDockerAvailable as k, promptMissingFields as l, saveOnboardState as m, installClientService as n, startServer as o, onboardCreate as p, isServiceSupported as r, isInteractive as s, getClientServiceStatus as t, formatCheckReport as u, checkDatabase as v, checkWebSocket as w, checkServerConfig as x, checkDocker as y };
|
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
|
|
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
|
-
export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, hasUser, isDockerAvailable, isInteractive, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveServerUrl, runMigrations, startServer, status, stopPostgres };
|
|
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 };
|