trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
123 lines (120 loc) • 4.35 kB
JavaScript
// src/registry/version-utils.ts
function compareVersions(a, b) {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const na = pa[i] ?? 0;
const nb = pb[i] ?? 0;
if (na > nb) return 1;
if (na < nb) return -1;
}
return 0;
}
function satisfies(version, range) {
if (range === "*" || range === "latest") return true;
if (range.startsWith(">=")) return compareVersions(version, range.slice(2)) >= 0;
if (range.startsWith(">")) return compareVersions(version, range.slice(1)) > 0;
if (range.startsWith("<=")) return compareVersions(version, range.slice(2)) <= 0;
if (range.startsWith("<")) return compareVersions(version, range.slice(1)) < 0;
if (range.startsWith("^")) {
const min = range.slice(1);
const parts = min.split(".");
const major = parseInt(parts[0], 10);
if (parts.length >= 2) {
const nextMajor = `${major + 1}.0.0`;
return compareVersions(version, min) >= 0 && compareVersions(version, nextMajor) < 0;
}
return version.startsWith(`${major}.`) || version === min;
}
if (range.startsWith("~")) {
const min = range.slice(1);
const parts = min.split(".");
if (parts.length >= 2) {
const nextMinor = `${parts[0]}.${parseInt(parts[1], 10) + 1}.0`;
return compareVersions(version, min) >= 0 && compareVersions(version, nextMinor) < 0;
}
return version === min;
}
return version === range;
}
function latestSatisfying(versions, range) {
const matching = versions.filter((v) => satisfies(v, range));
if (matching.length === 0) return null;
matching.sort((a, b) => compareVersions(b, a));
return matching[0];
}
// src/registry/lockfile.ts
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
import { join, dirname } from "path";
import { createHash } from "crypto";
var CURRENT_VERSION = 1;
var LOCKFILE_VERSION = "1.0.0";
var LOCKFILE_PATH = ".trellis/deps.json";
function computeContentHash(body) {
const canonical = JSON.stringify(JSON.parse(body), Object.keys(JSON.parse(body)).sort());
return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
}
function readLockfile(rootPath) {
const filePath = join(rootPath, LOCKFILE_PATH);
if (!existsSync(filePath)) return null;
const raw = readFileSync(filePath, "utf-8");
const data = JSON.parse(raw);
validateLockfile(data);
return data;
}
function writeLockfile(rootPath, data) {
const filePath = join(rootPath, LOCKFILE_PATH);
const dir = dirname(filePath);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
const tmpPath = filePath + ".tmp";
writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n");
renameSync(tmpPath, filePath);
}
function validateLockfile(data) {
if (typeof data !== "object" || data === null) throw new Error("Invalid lockfile: not an object");
const d = data;
if (d.version !== CURRENT_VERSION) throw new Error(`Invalid lockfile version: ${d.version}`);
if (d.lockfileVersion !== LOCKFILE_VERSION) throw new Error(`Invalid lockfile version: ${d.lockfileVersion}`);
if (typeof d.resolved !== "object" || d.resolved === null) throw new Error("Invalid lockfile: resolved is not an object");
if (typeof d.root !== "object" || d.root === null) throw new Error("Invalid lockfile: root is not an object");
}
function createLockfile() {
return {
version: CURRENT_VERSION,
lockfileVersion: LOCKFILE_VERSION,
resolved: {},
root: { depends: {} }
};
}
function removeFromLockfile(lockfile, name) {
if (!lockfile.resolved[name]) return false;
delete lockfile.resolved[name];
return true;
}
function findDependents(lockfile, name) {
const dependents = [];
const pkg = lockfile.resolved[name];
if (!pkg) return dependents;
const schemaIds = new Set(Object.keys(pkg.schemas));
for (const [pkgName, otherPkg] of Object.entries(lockfile.resolved)) {
if (pkgName === name) continue;
for (const schemaId of Object.keys(otherPkg.schemas)) {
if (schemaIds.has(schemaId)) {
dependents.push(pkgName);
break;
}
}
}
return dependents;
}
export {
compareVersions,
satisfies,
latestSatisfying,
computeContentHash,
readLockfile,
writeLockfile,
createLockfile,
removeFromLockfile,
findDependents
};