@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
63 lines (62 loc) • 3.26 kB
JavaScript
import path from "node:path";
import { z } from "zod";
import { registeredStoreIdentity, resolveAllStores, storePathExists } from "../store-registry.js";
import { ImmediatePulls } from "../sync/pull-now.js";
import { runGitWithin } from "../sync/pull.js";
import { parseRemoteUrl } from "./git-hosts.js";
import { BridgeError } from "./protocol.js";
import { ownerRequest } from "./request-origin.js";
const identitySchema = z.object({
storeId: z.string().min(1).max(128),
repository: z.string().max(513).regex(/^[^/\s]+\/[^/\s]+$/).transform(value => value.toLowerCase()),
branch: z.string().min(1).max(1024),
}).strict();
export class StorePulls {
primary;
pulls;
constructor(primary, pulls = new ImmediatePulls()) {
this.primary = primary;
this.pulls = pulls;
}
localStores() {
// A synced primary entry can carry another computer's path. The Hook's configured root wins.
return resolveAllStores(this.primary).slice(0, 64).map(entry => {
const local = entry.role === "primary" ? path.resolve(this.primary) : entry.path;
return { ...entry, path: local, available: storePathExists(local) };
}).filter(entry => entry.available);
}
/** A receiver's trusted local config may name an attachment ID; return the store's portable ID. */
request(storeId) {
const store = this.localStores().find(entry => entry.id === storeId || registeredStoreIdentity(entry.path) === storeId);
if (!store)
return undefined;
this.pulls.request(store.path);
return registeredStoreIdentity(store.path);
}
async route(request, data) {
if (!(ownerRequest(request, data) || (request.headers["x-phren-origin"] === "peer" && data.origin === undefined))) {
throw new BridgeError(403, "Only the owner or a peer Hook can request a pull.");
}
const identity = identitySchema.parse(data);
// Attachment IDs never cross computers. Only inspect checkouts carrying the requested identity.
const candidates = this.localStores().filter(entry => registeredStoreIdentity(entry.path) === identity.storeId);
for (const store of candidates) {
// Local Git metadata only, bounded; follow the same tracking branch pollStore will pull.
const refs = await runGitWithin(1000, store.path, ["for-each-ref", "--format=%(HEAD)%09%(upstream:remotename)%09%(upstream:remoteref)", "refs/heads/"]);
if (!refs.ok)
continue;
const current = refs.output.split("\n").find(line => line.startsWith("*\t"));
const [, remote, branch] = current?.split("\t") ?? [];
if (!remote || remote === "." || branch !== `refs/heads/${identity.branch}`)
continue;
const url = await runGitWithin(1000, store.path, ["remote", "get-url", "--", remote]);
const repository = url.ok ? parseRemoteUrl(url.output)?.path.toLowerCase() : undefined;
if (repository !== identity.repository)
continue;
this.pulls.request(store.path);
return { ok: true };
}
throw new BridgeError(404, "Not found.");
}
stop() { this.pulls.stop(); }
}