@h-rig/server 0.0.6-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +14 -0
  2. package/dist/src/bootstrap.js +161 -0
  3. package/dist/src/index.js +13153 -0
  4. package/dist/src/inspector/agent-runtime.js +1077 -0
  5. package/dist/src/inspector/analysis.js +41 -0
  6. package/dist/src/inspector/discovery.js +137 -0
  7. package/dist/src/inspector/journal.js +518 -0
  8. package/dist/src/inspector/mission.js +562 -0
  9. package/dist/src/inspector/prompt.js +97 -0
  10. package/dist/src/inspector/provider-session.js +65 -0
  11. package/dist/src/inspector/reconcile.js +118 -0
  12. package/dist/src/inspector/review.js +13 -0
  13. package/dist/src/inspector/service.js +1759 -0
  14. package/dist/src/inspector/skills.js +155 -0
  15. package/dist/src/inspector/tools.js +1592 -0
  16. package/dist/src/inspector/types.js +1 -0
  17. package/dist/src/inspector/upstream-sync.js +479 -0
  18. package/dist/src/orchestration.js +402 -0
  19. package/dist/src/remote.js +123 -0
  20. package/dist/src/scheduler.js +84 -0
  21. package/dist/src/server-helpers/broadcasters.js +161 -0
  22. package/dist/src/server-helpers/conversation-snapshot.js +382 -0
  23. package/dist/src/server-helpers/event-emitter.js +41 -0
  24. package/dist/src/server-helpers/github-auth-store.js +155 -0
  25. package/dist/src/server-helpers/github-credentials.js +38 -0
  26. package/dist/src/server-helpers/github-project-status-sync.js +196 -0
  27. package/dist/src/server-helpers/github-projects.js +147 -0
  28. package/dist/src/server-helpers/github-reconciler.js +89 -0
  29. package/dist/src/server-helpers/http-router.js +3781 -0
  30. package/dist/src/server-helpers/http-utils.js +135 -0
  31. package/dist/src/server-helpers/inspector-agent-lifecycle.js +104 -0
  32. package/dist/src/server-helpers/inspector-jobs.js +4145 -0
  33. package/dist/src/server-helpers/issue-analysis.js +362 -0
  34. package/dist/src/server-helpers/normalizers.js +31 -0
  35. package/dist/src/server-helpers/notifications.js +96 -0
  36. package/dist/src/server-helpers/orchestration-ops.js +287 -0
  37. package/dist/src/server-helpers/orchestration.js +39 -0
  38. package/dist/src/server-helpers/plugin-host-cache.js +86 -0
  39. package/dist/src/server-helpers/project-fs-ops.js +194 -0
  40. package/dist/src/server-helpers/project-registry.js +124 -0
  41. package/dist/src/server-helpers/queue-state.js +78 -0
  42. package/dist/src/server-helpers/remote-checkout.js +140 -0
  43. package/dist/src/server-helpers/remote-snapshots.js +119 -0
  44. package/dist/src/server-helpers/run-io.js +262 -0
  45. package/dist/src/server-helpers/run-mutations.js +1784 -0
  46. package/dist/src/server-helpers/run-steering.js +176 -0
  47. package/dist/src/server-helpers/run-writers.js +75 -0
  48. package/dist/src/server-helpers/server-paths.js +27 -0
  49. package/dist/src/server-helpers/snapshot-orchestrator.js +832 -0
  50. package/dist/src/server-helpers/snapshot-service.js +1143 -0
  51. package/dist/src/server-helpers/summaries.js +126 -0
  52. package/dist/src/server-helpers/task-config.js +50 -0
  53. package/dist/src/server-helpers/task-projection.js +98 -0
  54. package/dist/src/server-helpers/terminal-runtime.js +156 -0
  55. package/dist/src/server-helpers/terminal-sessions.js +22 -0
  56. package/dist/src/server-helpers/validation-failure.js +31 -0
  57. package/dist/src/server-helpers/ws-router.js +1308 -0
  58. package/dist/src/server.js +12628 -0
  59. package/dist/src/websocket.js +63 -0
  60. package/package.json +33 -0
@@ -0,0 +1,124 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/project-registry.ts
3
+ import { createHash } from "crypto";
4
+ import { spawnSync } from "child_process";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
6
+ import { dirname, resolve } from "path";
7
+ function normalizeRepoSlug(value) {
8
+ const trimmed = value.trim();
9
+ return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
10
+ }
11
+ function registryPath(projectRoot) {
12
+ return resolve(projectRoot, ".rig", "state", "projects.json");
13
+ }
14
+ function readRegistry(projectRoot) {
15
+ const path = registryPath(projectRoot);
16
+ if (!existsSync(path))
17
+ return {};
18
+ try {
19
+ const payload = JSON.parse(readFileSync(path, "utf8"));
20
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
21
+ return {};
22
+ const projects = payload.projects;
23
+ return projects && typeof projects === "object" && !Array.isArray(projects) ? projects : {};
24
+ } catch {
25
+ return {};
26
+ }
27
+ }
28
+ function writeRegistry(projectRoot, projects) {
29
+ const path = registryPath(projectRoot);
30
+ mkdirSync(dirname(path), { recursive: true });
31
+ writeFileSync(path, `${JSON.stringify({ projects }, null, 2)}
32
+ `, "utf8");
33
+ }
34
+ function resolveConfigPath(projectRoot) {
35
+ for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
36
+ const path = resolve(projectRoot, name);
37
+ if (existsSync(path))
38
+ return path;
39
+ }
40
+ return null;
41
+ }
42
+ function hashFile(path) {
43
+ if (!path)
44
+ return null;
45
+ try {
46
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function buildConfigStatus(projectRoot) {
52
+ return resolveConfigPath(projectRoot) ? "valid" : "missing";
53
+ }
54
+ function readDefaultBranch(projectRoot) {
55
+ const origin = spawnSync("git", ["-C", projectRoot, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { encoding: "utf8", timeout: 5000 });
56
+ if (origin.status === 0 && origin.stdout.trim())
57
+ return origin.stdout.trim().replace(/^origin\//, "");
58
+ const head = spawnSync("git", ["-C", projectRoot, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8", timeout: 5000 });
59
+ return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
60
+ }
61
+ function buildRunSummary(projectRoot) {
62
+ const runsDir = resolve(projectRoot, ".rig", "runs");
63
+ try {
64
+ const runs = readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
65
+ try {
66
+ const run = JSON.parse(readFileSync(resolve(runsDir, entry.name, "run.json"), "utf8"));
67
+ return [{ runId: typeof run.runId === "string" ? run.runId : entry.name, status: typeof run.status === "string" ? run.status : "unknown", updatedAt: typeof run.updatedAt === "string" ? run.updatedAt : "" }];
68
+ } catch {
69
+ return [];
70
+ }
71
+ });
72
+ const active = runs.filter((run) => !["completed", "failed", "cancelled", "canceled", "stopped", "closed", "merged", "needs_attention"].includes(run.status.toLowerCase())).length;
73
+ const latest = runs.toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0]?.runId ?? null;
74
+ return { total: runs.length, active, latestRunId: latest };
75
+ } catch {
76
+ return { total: 0, active: 0, latestRunId: null };
77
+ }
78
+ }
79
+ function getProjectRecord(projectRoot, repoSlug) {
80
+ const normalized = normalizeRepoSlug(repoSlug);
81
+ if (!normalized)
82
+ return null;
83
+ return readRegistry(projectRoot)[normalized] ?? null;
84
+ }
85
+ function upsertProjectRecord(projectRoot, input) {
86
+ const repoSlug = normalizeRepoSlug(input.repoSlug);
87
+ if (!repoSlug)
88
+ throw new Error(`Invalid repo slug: ${input.repoSlug}`);
89
+ const now = new Date().toISOString();
90
+ const projects = readRegistry(projectRoot);
91
+ const existing = projects[repoSlug];
92
+ const checkout = input.checkout ? { ...input.checkout, createdAt: input.checkout.createdAt ?? now } : null;
93
+ const checkouts = checkout ? [...(existing?.checkouts ?? []).filter((entry) => !(entry.kind === checkout.kind && entry.path === checkout.path)), checkout] : existing?.checkouts ?? [];
94
+ const configPath = resolveConfigPath(projectRoot);
95
+ const configStatus = buildConfigStatus(projectRoot);
96
+ const authStatus = input.githubAuthStatus ?? existing?.github.authStatus ?? "unauthenticated";
97
+ const record = {
98
+ repoSlug,
99
+ defaultBranch: readDefaultBranch(projectRoot) ?? existing?.defaultBranch ?? null,
100
+ checkouts,
101
+ configStatus,
102
+ config: { status: configStatus, hash: hashFile(configPath), path: configPath },
103
+ github: { authStatus },
104
+ taskSource: existing?.taskSource ?? { health: "unknown", lastCheckedAt: null },
105
+ runs: buildRunSummary(projectRoot),
106
+ workers: existing?.workers ?? { localAvailable: true, remoteAvailable: false, remoteCount: 0 },
107
+ operatorPermissions: existing?.operatorPermissions ?? { canRead: true, canRun: authStatus === "authenticated", canControl: authStatus === "authenticated" },
108
+ createdAt: existing?.createdAt ?? now,
109
+ updatedAt: now
110
+ };
111
+ projects[repoSlug] = record;
112
+ writeRegistry(projectRoot, projects);
113
+ return record;
114
+ }
115
+ function linkProjectCheckout(projectRoot, repoSlug, checkout) {
116
+ return upsertProjectRecord(projectRoot, { repoSlug, checkout });
117
+ }
118
+ export {
119
+ upsertProjectRecord,
120
+ normalizeRepoSlug,
121
+ linkProjectCheckout,
122
+ getProjectRecord,
123
+ buildConfigStatus
124
+ };
@@ -0,0 +1,78 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/queue-state.ts
3
+ import { resolve as resolve2 } from "path";
4
+ import { readJsonFile, writeJsonFile } from "@rig/runtime/control-plane/authority-files";
5
+
6
+ // packages/server/src/server-helpers/normalizers.ts
7
+ function normalizeString(value) {
8
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
9
+ }
10
+
11
+ // packages/server/src/server-helpers/server-paths.ts
12
+ import { dirname, resolve } from "path";
13
+ import { resolveMonorepoRoot } from "@rig/runtime/control-plane/native/utils";
14
+ function resolveServerAuthorityPaths(projectRoot) {
15
+ const taskWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
16
+ const explicitStateDir = process.env.RIG_STATE_DIR?.trim();
17
+ const explicitLogsDir = process.env.RIG_LOGS_DIR?.trim();
18
+ const explicitSessionFile = process.env.RIG_SESSION_FILE?.trim();
19
+ const monorepoRoot = resolveMonorepoRoot(projectRoot);
20
+ const stateRoot = taskWorkspace ? resolve(taskWorkspace, ".rig") : explicitStateDir ? dirname(resolve(explicitStateDir)) : explicitLogsDir ? dirname(resolve(explicitLogsDir)) : explicitSessionFile ? dirname(dirname(resolve(explicitSessionFile))) : resolve(monorepoRoot, ".rig");
21
+ const stateDir = explicitStateDir ? resolve(explicitStateDir) : resolve(stateRoot, "state");
22
+ const logsDir = explicitLogsDir ? resolve(explicitLogsDir) : resolve(stateRoot, "logs");
23
+ const sessionFile = explicitSessionFile ? resolve(explicitSessionFile) : resolve(stateRoot, "session", "session.json");
24
+ const artifactsDir = taskWorkspace ? resolve(taskWorkspace, "artifacts") : resolve(monorepoRoot, "artifacts");
25
+ return {
26
+ stateRoot,
27
+ stateDir,
28
+ logsDir,
29
+ controlPlaneEventsFile: resolve(logsDir, "control-plane.events.jsonl"),
30
+ sessionFile,
31
+ artifactsDir
32
+ };
33
+ }
34
+
35
+ // packages/server/src/server-helpers/queue-state.ts
36
+ function resolveQueueStatePath(projectRoot) {
37
+ return resolve2(resolveServerAuthorityPaths(projectRoot).stateDir, "task-queue.json");
38
+ }
39
+ function readQueueState(projectRoot) {
40
+ const queue = readJsonFile(resolveQueueStatePath(projectRoot), null);
41
+ if (!Array.isArray(queue))
42
+ return [];
43
+ return queue.filter((entry) => entry && typeof entry === "object").map((entry, index) => ({
44
+ taskId: normalizeString(entry.taskId),
45
+ score: typeof entry.score === "number" ? Math.max(0, Math.trunc(entry.score)) : 0,
46
+ unblockCount: typeof entry.unblockCount === "number" ? Math.max(0, Math.trunc(entry.unblockCount)) : 0,
47
+ position: typeof entry.position === "number" ? Math.max(0, Math.trunc(entry.position)) : index
48
+ })).filter((entry) => Boolean(entry.taskId)).sort((left, right) => right.score - left.score || left.position - right.position).map((entry, index) => ({ ...entry, position: index }));
49
+ }
50
+ function writeQueueState(projectRoot, queue) {
51
+ writeJsonFile(resolveQueueStatePath(projectRoot), queue);
52
+ }
53
+ function enqueueTaskState(projectRoot, taskId, score) {
54
+ const queue = readQueueState(projectRoot).filter((entry) => entry.taskId !== taskId);
55
+ const next = [
56
+ ...queue,
57
+ {
58
+ taskId,
59
+ score: Math.max(0, Math.trunc(score)),
60
+ unblockCount: 0,
61
+ position: queue.length
62
+ }
63
+ ].sort((left, right) => right.score - left.score || left.position - right.position).map((entry, index) => ({ ...entry, position: index }));
64
+ writeQueueState(projectRoot, next);
65
+ return next;
66
+ }
67
+ function dequeueTaskState(projectRoot, taskId) {
68
+ const next = readQueueState(projectRoot).filter((entry) => entry.taskId !== taskId).map((entry, index) => ({ ...entry, position: index }));
69
+ writeQueueState(projectRoot, next);
70
+ return next;
71
+ }
72
+ export {
73
+ writeQueueState,
74
+ resolveQueueStatePath,
75
+ readQueueState,
76
+ enqueueTaskState,
77
+ dequeueTaskState
78
+ };
@@ -0,0 +1,140 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/remote-checkout.ts
3
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
4
+ import { dirname, isAbsolute, relative, resolve } from "path";
5
+ function safeSlugSegments(repoSlug) {
6
+ const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
7
+ if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
8
+ throw new Error("repoSlug must be owner/repo");
9
+ }
10
+ return segments;
11
+ }
12
+ function safeCheckoutKey(value) {
13
+ const raw = value?.trim();
14
+ if (!raw)
15
+ return null;
16
+ const safe = raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
17
+ return safe || null;
18
+ }
19
+ function repoSlugPath(baseDir, repoSlug, checkoutKey) {
20
+ const key = safeCheckoutKey(checkoutKey);
21
+ return resolve(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
22
+ }
23
+ function sanitizeSnapshotId(value, fallback) {
24
+ const raw = (value ?? fallback).trim();
25
+ const safe = raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
26
+ return safe || fallback;
27
+ }
28
+ function assertWithinRoot(root, relativePath) {
29
+ if (!relativePath || isAbsolute(relativePath) || relativePath.includes("\x00")) {
30
+ throw new Error(`Invalid snapshot file path: ${relativePath}`);
31
+ }
32
+ const normalizedRelative = relativePath.replace(/\\/g, "/");
33
+ const segments = normalizedRelative.split("/");
34
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
35
+ throw new Error(`Unsafe snapshot file path: ${relativePath}`);
36
+ }
37
+ const target = resolve(root, ...segments);
38
+ const rel = relative(root, target);
39
+ if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute(rel)) {
40
+ throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
41
+ }
42
+ return target;
43
+ }
44
+ function decodeSnapshotArchive(archive) {
45
+ if (!archive || typeof archive !== "object" || archive.version !== 1 || !Array.isArray(archive.files)) {
46
+ throw new Error("Unsupported snapshot archive payload");
47
+ }
48
+ return archive;
49
+ }
50
+ function parseSnapshotArchiveContentBase64(contentBase64) {
51
+ const json = Buffer.from(contentBase64, "base64").toString("utf8");
52
+ return decodeSnapshotArchive(JSON.parse(json));
53
+ }
54
+ function extractUploadedSnapshotArchive(input) {
55
+ const archive = decodeSnapshotArchive(input.archive);
56
+ const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
57
+ const checkoutPath = resolve(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
58
+ mkdirSync(checkoutPath, { recursive: true });
59
+ for (const file of archive.files) {
60
+ if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
61
+ throw new Error("Invalid snapshot archive file entry");
62
+ }
63
+ const target = assertWithinRoot(checkoutPath, file.path);
64
+ mkdirSync(dirname(target), { recursive: true });
65
+ writeFileSync(target, Buffer.from(file.contentBase64, "base64"));
66
+ }
67
+ writeFileSync(resolve(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
68
+ repoSlug: input.repoSlug,
69
+ snapshotId,
70
+ fileCount: archive.files.length,
71
+ archiveCreatedAt: archive.createdAt ?? null,
72
+ extractedAt: (input.now?.() ?? new Date).toISOString()
73
+ }, null, 2)}
74
+ `, "utf8");
75
+ return { kind: "uploaded-snapshot", path: checkoutPath, fileCount: archive.files.length, snapshotId };
76
+ }
77
+ async function runChecked(command, args, cwd, env) {
78
+ const result = await command(args, cwd || env ? { ...cwd ? { cwd } : {}, ...env ? { env } : {} } : undefined);
79
+ if (result.exitCode !== 0) {
80
+ throw new Error(`${args.join(" ")} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim());
81
+ }
82
+ return result;
83
+ }
84
+ function gitCredentialConfig(token) {
85
+ const clean = token?.trim();
86
+ if (!clean)
87
+ return { args: [] };
88
+ return {
89
+ args: [
90
+ "-c",
91
+ "credential.helper=",
92
+ "-c",
93
+ 'credential.helper=!f() { test "$1" = get && echo username=x-access-token && echo password="$RIG_GIT_CREDENTIAL_TOKEN"; }; f'
94
+ ],
95
+ env: {
96
+ RIG_GIT_CREDENTIAL_TOKEN: clean,
97
+ GIT_TERMINAL_PROMPT: "0"
98
+ }
99
+ };
100
+ }
101
+ async function prepareRemoteCheckout(input) {
102
+ const exists = input.exists ?? existsSync;
103
+ const strategy = input.strategy;
104
+ if (strategy.kind === "uploaded-snapshot") {
105
+ return extractUploadedSnapshotArchive({
106
+ repoSlug: strategy.repoSlug,
107
+ baseDir: strategy.baseDir,
108
+ archive: strategy.archive,
109
+ snapshotId: strategy.snapshotId,
110
+ checkoutKey: strategy.checkoutKey
111
+ });
112
+ }
113
+ if (strategy.kind === "existing-path") {
114
+ const checkoutPath2 = resolve(strategy.path);
115
+ if (!exists(checkoutPath2)) {
116
+ throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
117
+ }
118
+ await runChecked(input.command, ["git", "-C", checkoutPath2, "rev-parse", "--is-inside-work-tree"]);
119
+ return { kind: "existing-path", path: checkoutPath2 };
120
+ }
121
+ const checkoutPath = repoSlugPath(strategy.baseDir, strategy.repoSlug, strategy.checkoutKey);
122
+ const credentials = gitCredentialConfig(strategy.credentialToken);
123
+ if (!exists(checkoutPath)) {
124
+ await runChecked(input.command, ["git", ...credentials.args, "clone", strategy.repoUrl, checkoutPath], undefined, credentials.env);
125
+ }
126
+ if (strategy.kind === "current-ref") {
127
+ const ref = strategy.ref.trim();
128
+ if (!ref)
129
+ throw new Error("current-ref checkout requires a ref");
130
+ await runChecked(input.command, ["git", "-C", checkoutPath, ...credentials.args, "fetch", "origin", ref], undefined, credentials.env);
131
+ await runChecked(input.command, ["git", "-C", checkoutPath, "checkout", ref]);
132
+ return { kind: "current-ref", path: checkoutPath, ref };
133
+ }
134
+ return { kind: "managed-clone", path: checkoutPath };
135
+ }
136
+ export {
137
+ prepareRemoteCheckout,
138
+ parseSnapshotArchiveContentBase64,
139
+ extractUploadedSnapshotArchive
140
+ };
@@ -0,0 +1,119 @@
1
+ // @bun
2
+ // packages/server/src/server-helpers/remote-snapshots.ts
3
+ import { listAuthorityRemoteEndpoints } from "@rig/runtime/control-plane/authority-files";
4
+
5
+ // packages/server/src/server-helpers/normalizers.ts
6
+ function normalizeString(value) {
7
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
8
+ }
9
+
10
+ // packages/server/src/server-helpers/remote-snapshots.ts
11
+ function toRemoteConnectionStatus(status) {
12
+ switch (status) {
13
+ case "connected":
14
+ case "connecting":
15
+ case "authenticating":
16
+ case "reconnecting":
17
+ case "error":
18
+ case "disconnected":
19
+ return status;
20
+ case "ready":
21
+ case "busy":
22
+ case "degraded":
23
+ case "draining":
24
+ return "connected";
25
+ case "registering":
26
+ return "connecting";
27
+ case "offline":
28
+ case "quarantined":
29
+ return "error";
30
+ default:
31
+ return "disconnected";
32
+ }
33
+ }
34
+ function hostToRemoteEndpoint(host) {
35
+ let hostName = "127.0.0.1";
36
+ let port = 7890;
37
+ try {
38
+ const url = new URL(host.baseUrl);
39
+ hostName = url.hostname || hostName;
40
+ port = Number.parseInt(url.port || "80", 10) || port;
41
+ } catch {}
42
+ return {
43
+ id: host.hostId,
44
+ alias: host.name,
45
+ host: hostName,
46
+ port,
47
+ token: "",
48
+ tokenConfigured: false,
49
+ autoConnect: true,
50
+ addedAt: host.registeredAt,
51
+ lastConnectedAt: host.lastHeartbeatAt
52
+ };
53
+ }
54
+ function buildRemoteEndpointSnapshot(projectRoot, remoteHosts) {
55
+ const authorityEndpoints = listAuthorityRemoteEndpoints(projectRoot).map((entry) => ({
56
+ id: entry.id,
57
+ alias: entry.alias,
58
+ host: entry.host,
59
+ port: entry.port,
60
+ token: "",
61
+ tokenConfigured: entry.token.trim().length > 0,
62
+ autoConnect: entry.autoConnect,
63
+ addedAt: entry.addedAt,
64
+ lastConnectedAt: entry.lastConnectedAt
65
+ }));
66
+ const hostEndpoints = Array.from(remoteHosts.values()).map(hostToRemoteEndpoint);
67
+ const deduped = new Map;
68
+ for (const entry of [...authorityEndpoints, ...hostEndpoints]) {
69
+ deduped.set(entry.id, entry);
70
+ }
71
+ return Array.from(deduped.values()).sort((left, right) => left.alias.localeCompare(right.alias));
72
+ }
73
+ function buildRemoteConnectionSnapshot(remoteHosts, remoteConnections, endpoints) {
74
+ const connections = new Map;
75
+ for (const [endpointId, summary] of remoteConnections.entries()) {
76
+ connections.set(endpointId, summary);
77
+ }
78
+ for (const host of remoteHosts.values()) {
79
+ if (connections.has(host.hostId))
80
+ continue;
81
+ connections.set(host.hostId, {
82
+ endpointId: host.hostId,
83
+ status: toRemoteConnectionStatus(host.status),
84
+ error: host.status === "offline" || host.status === "quarantined" ? host.status : null,
85
+ connectedAt: host.lastHeartbeatAt,
86
+ tokenExpiresAt: null,
87
+ latencyMs: null,
88
+ subscribedEvents: []
89
+ });
90
+ }
91
+ for (const endpoint of endpoints) {
92
+ if (connections.has(endpoint.id))
93
+ continue;
94
+ connections.set(endpoint.id, {
95
+ endpointId: endpoint.id,
96
+ status: "disconnected",
97
+ error: null,
98
+ connectedAt: endpoint.lastConnectedAt,
99
+ tokenExpiresAt: null,
100
+ latencyMs: null,
101
+ subscribedEvents: []
102
+ });
103
+ }
104
+ return Array.from(connections.values()).sort((left, right) => String(left.endpointId).localeCompare(String(right.endpointId)));
105
+ }
106
+ function normalizeIsoTimestamp(value) {
107
+ const normalized = normalizeString(value);
108
+ if (!normalized)
109
+ return null;
110
+ const timestamp = Date.parse(normalized);
111
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
112
+ }
113
+ export {
114
+ toRemoteConnectionStatus,
115
+ normalizeIsoTimestamp,
116
+ hostToRemoteEndpoint,
117
+ buildRemoteEndpointSnapshot,
118
+ buildRemoteConnectionSnapshot
119
+ };