Files
Jyotisha/frontend/src/lib/skill-package-registry.ts
T
Jesse_Chen d35828e76d
Independent Staging Quality Gate / validate (push) Successful in 9m18s
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(consult): read the live skill tree instead of a hash-pinned snapshot
Manual SKILL.md updates were blocked by registry sha256 and a byte-equal versions/ gate. Consult now loads the operator-maintained tree; rectification and personal-report stay hashed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 10:25:55 +08:00

890 lines
27 KiB
TypeScript

import { createHash } from "node:crypto";
import {
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
realpathSync,
statSync,
symlinkSync,
} from "node:fs";
import {
basename,
isAbsolute,
join,
relative,
resolve,
sep,
win32,
} from "node:path";
import { tmpdir } from "node:os";
export type SkillPackageStatus = "active" | "deprecated" | "blocked";
export interface SkillPackageIdentity {
name: string;
version: string;
sha256: string;
sourceCommit: string | null;
packagePath: string;
status: SkillPackageStatus;
}
export type ResolvedSkillPackageIdentity = SkillPackageIdentity & {
resolvedPath: string;
};
export interface SkillPackageRegistryOptions {
projectRoot?: string;
registryPath?: string;
}
interface SkillPackageRegistryDocument {
schemaVersion?: number;
packages: SkillPackageIdentity[];
}
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
const GIT_COMMIT_PATTERN = /^[0-9a-f]{40}$/;
const NAME_PATTERN = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
const VERSION_PATTERN =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
const STATUS_VALUES = new Set<SkillPackageStatus>([
"active",
"deprecated",
"blocked",
]);
const IDENTITY_KEYS = new Set([
"name",
"version",
"sha256",
"sourceCommit",
"packagePath",
"status",
]);
const DEFAULT_REGISTRY_PATH = "skills/skill-package-registry.json";
const SOURCE_TREE_PROJECT_ROOT = process.env.JYOTISHA_PROJECT_ROOT
?? (existsSync(resolve(process.cwd(), DEFAULT_REGISTRY_PATH))
? process.cwd()
: resolve(process.cwd(), ".."));
const PACKAGE_HASH_DOMAIN = "jyotisha-skill-package-v1\0";
const RUNTIME_SKILL_ROOT = mkdtempSync(join(tmpdir(), "jyotisha-skill-runtime-"));
export class SkillPackageRegistryError extends Error {
constructor(message: string) {
super(message);
this.name = "SkillPackageRegistryError";
}
}
function fail(message: string): never {
throw new SkillPackageRegistryError(message);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function assertString(
value: unknown,
field: keyof SkillPackageIdentity,
index: number,
): string {
if (typeof value !== "string" || value.length === 0) {
return fail(`Registry package ${index} has invalid or missing ${field}`);
}
return value;
}
function validatePackagePath(packagePath: string, index: number): void {
if (
packagePath.includes("\0") ||
packagePath.includes("\\") ||
isAbsolute(packagePath) ||
win32.isAbsolute(packagePath)
) {
fail(`Registry package ${index} has an unsafe packagePath`);
}
const segments = packagePath.split("/");
if (
segments.length === 0 ||
segments.some(
(segment) => segment === "" || segment === "." || segment === "..",
)
) {
fail(`Registry package ${index} has an unsafe packagePath`);
}
}
function parseIdentity(value: unknown, index: number): SkillPackageIdentity {
if (!isRecord(value)) {
return fail(`Registry package ${index} must be an object`);
}
const unknownKeys = Object.keys(value).filter((key) => !IDENTITY_KEYS.has(key));
if (unknownKeys.length > 0) {
fail(
`Registry package ${index} contains unknown fields: ${unknownKeys.join(", ")}`,
);
}
const name = assertString(value.name, "name", index);
const version = assertString(value.version, "version", index);
const sha256 = assertString(value.sha256, "sha256", index);
const sourceCommit = value.sourceCommit === null
? null
: assertString(value.sourceCommit, "sourceCommit", index);
const packagePath = assertString(value.packagePath, "packagePath", index);
const status = assertString(value.status, "status", index);
if (!NAME_PATTERN.test(name)) {
fail(`Registry package ${index} has an invalid name`);
}
if (!VERSION_PATTERN.test(version)) {
fail(`Registry package ${index} has an invalid version`);
}
if (!SHA256_PATTERN.test(sha256)) {
fail(`Registry package ${index} has an invalid sha256`);
}
if (sourceCommit !== null && !GIT_COMMIT_PATTERN.test(sourceCommit)) {
fail(`Registry package ${index} has an invalid sourceCommit`);
}
validatePackagePath(packagePath, index);
if (!STATUS_VALUES.has(status as SkillPackageStatus)) {
fail(`Registry package ${index} has an invalid status`);
}
return Object.freeze({
name,
version,
sha256,
sourceCommit,
packagePath,
status: status as SkillPackageStatus,
});
}
function parseRegistry(contents: string): SkillPackageRegistryDocument {
let value: unknown;
try {
value = JSON.parse(contents);
} catch (error) {
fail(
`Skill package registry is not valid JSON: ${
error instanceof Error ? error.message : "unknown parse error"
}`,
);
}
if (!isRecord(value) || !Array.isArray(value.packages)) {
return fail("Skill package registry must contain a packages array");
}
if (value.schemaVersion !== undefined && value.schemaVersion !== 1) {
fail("Skill package registry has an unsupported schemaVersion");
}
const packages = value.packages.map(parseIdentity);
const exactIdentities = new Set<string>();
const activeNames = new Set<string>();
for (const identity of packages) {
const exactKey = `${identity.name}\0${identity.version}\0${identity.sha256}`;
if (exactIdentities.has(exactKey)) {
fail(
`Registry contains duplicate identity ${identity.name}@${identity.version}#${identity.sha256}`,
);
}
exactIdentities.add(exactKey);
if (identity.status === "active") {
if (activeNames.has(identity.name)) {
fail(`Registry contains duplicate active package ${identity.name}`);
}
activeNames.add(identity.name);
}
}
return { schemaVersion: value.schemaVersion as number | undefined, packages };
}
function isInside(rootPath: string, candidatePath: string): boolean {
const relativePath = relative(rootPath, candidatePath);
return (
relativePath === "" ||
(!relativePath.startsWith(`..${sep}`) &&
relativePath !== ".." &&
!isAbsolute(relativePath))
);
}
function resolveExistingPath(pathValue: string, label: string): string {
try {
return realpathSync(pathValue);
} catch (error) {
return fail(
`${label} does not exist or cannot be resolved: ${
error instanceof Error ? error.message : "unknown filesystem error"
}`,
);
}
}
function assertDirectory(pathValue: string, label: string): void {
let stats;
try {
stats = statSync(pathValue);
} catch (error) {
fail(
`${label} cannot be inspected: ${
error instanceof Error ? error.message : "unknown filesystem error"
}`,
);
}
if (!stats.isDirectory()) {
fail(`${label} must be a directory`);
}
}
function assertRegularFile(pathValue: string, label: string): void {
let stats;
try {
stats = statSync(pathValue);
} catch (error) {
fail(
`${label} cannot be inspected: ${
error instanceof Error ? error.message : "unknown filesystem error"
}`,
);
}
if (!stats.isFile()) {
fail(`${label} must be a regular file`);
}
}
function comparePathNames(left: string, right: string): number {
if (left === right) return 0;
return left < right ? -1 : 1;
}
function updateLengthPrefixed(
hash: ReturnType<typeof createHash>,
label: string,
value: string | Buffer,
): void {
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : value;
hash.update(`${label}:${bytes.length}\0`, "utf8");
hash.update(bytes);
hash.update("\0", "utf8");
}
/**
* Hash the complete immutable package tree, not just SKILL.md. Paths,
* executable bits, and file bytes are framed deterministically so references,
* scripts, and assets are part of the runtime identity.
*/
export function computeSkillPackageSha256(packageDirectory: string): string {
const resolvedDirectory = resolveExistingPath(
resolve(packageDirectory),
"Skill package directory",
);
assertDirectory(resolvedDirectory, "Skill package directory");
const hash = createHash("sha256");
hash.update(PACKAGE_HASH_DOMAIN, "utf8");
let fileCount = 0;
const visit = (directory: string, relativeDirectory: string): void => {
let entries;
try {
entries = readdirSync(directory, { withFileTypes: true });
} catch (error) {
fail(
`Skill package directory cannot be read: ${
error instanceof Error ? error.message : "unknown filesystem error"
}`,
);
}
entries.sort((left, right) => comparePathNames(left.name, right.name));
for (const entry of entries) {
const absolutePath = resolve(directory, entry.name);
const relativePath = relativeDirectory
? `${relativeDirectory}/${entry.name}`
: entry.name;
let stats;
try {
stats = lstatSync(absolutePath);
} catch (error) {
fail(
`Skill package entry ${relativePath} cannot be inspected: ${
error instanceof Error ? error.message : "unknown filesystem error"
}`,
);
}
if (stats.isSymbolicLink()) {
fail(`Skill package entry ${relativePath} must not be a symbolic link`);
}
if (stats.isDirectory()) {
visit(absolutePath, relativePath);
continue;
}
if (!stats.isFile()) {
fail(`Skill package entry ${relativePath} must be a regular file`);
}
let bytes: Buffer;
try {
bytes = readFileSync(absolutePath);
} catch (error) {
fail(
`Skill package entry ${relativePath} cannot be read: ${
error instanceof Error ? error.message : "unknown filesystem error"
}`,
);
}
updateLengthPrefixed(hash, "path", relativePath);
updateLengthPrefixed(hash, "executable", stats.mode & 0o111 ? "1" : "0");
updateLengthPrefixed(hash, "bytes", bytes);
fileCount += 1;
}
};
visit(resolvedDirectory, "");
if (fileCount === 0) {
fail("Skill package directory must contain at least one regular file");
}
return hash.digest("hex");
}
function normalizeLookupName(name: string): string {
if (!NAME_PATTERN.test(name)) {
fail("Skill package name is invalid");
}
return name;
}
function normalizeLookupVersion(version: string): string {
if (!VERSION_PATTERN.test(version)) {
fail("Skill package version is invalid");
}
return version;
}
function normalizeLookupHash(sha256: string): string {
if (!SHA256_PATTERN.test(sha256)) {
fail("Skill package sha256 is invalid");
}
return sha256;
}
export class SkillPackageRegistry {
readonly projectRoot: string;
readonly registryPath: string;
constructor(options: SkillPackageRegistryOptions = {}) {
const requestedProjectRoot = resolve(
options.projectRoot ?? process.env.JYOTISHA_PROJECT_ROOT ?? SOURCE_TREE_PROJECT_ROOT,
);
this.projectRoot = resolveExistingPath(
requestedProjectRoot,
"Skill registry project root",
);
assertDirectory(this.projectRoot, "Skill registry project root");
const requestedRegistryPath = options.registryPath
? isAbsolute(options.registryPath)
? resolve(options.registryPath)
: resolve(this.projectRoot, options.registryPath)
: resolve(this.projectRoot, DEFAULT_REGISTRY_PATH);
const registryPath = resolveExistingPath(
requestedRegistryPath,
"Skill package registry",
);
if (!isInside(this.projectRoot, registryPath)) {
fail("Skill package registry escapes the project root");
}
assertRegularFile(registryPath, "Skill package registry");
this.registryPath = registryPath;
}
resolveActiveSkillPackage(name: string): ResolvedSkillPackageIdentity {
return this.resolveActive(name);
}
resolveActive(name: string): ResolvedSkillPackageIdentity {
const normalizedName = normalizeLookupName(name);
const registry = this.loadRegistry();
const identity = registry.packages.find(
(candidate) =>
candidate.name === normalizedName && candidate.status === "active",
);
if (!identity) {
return fail(`No active skill package found for ${normalizedName}`);
}
return this.verifyIdentity(identity);
}
resolveExactSkillPackage(
name: string,
version: string,
sha256: string,
): ResolvedSkillPackageIdentity {
return this.resolveExact(name, version, sha256);
}
resolveExact(
name: string,
version: string,
sha256: string,
): ResolvedSkillPackageIdentity {
const normalizedName = normalizeLookupName(name);
const normalizedVersion = normalizeLookupVersion(version);
const normalizedHash = normalizeLookupHash(sha256);
const registry = this.loadRegistry();
const identity = registry.packages.find(
(candidate) =>
candidate.name === normalizedName &&
candidate.version === normalizedVersion &&
candidate.sha256 === normalizedHash,
);
if (!identity) {
return fail(
`No exact skill package found for ${normalizedName}@${normalizedVersion}#${normalizedHash}`,
);
}
if (identity.status === "blocked") {
return fail(
`Skill package ${normalizedName}@${normalizedVersion} is blocked`,
);
}
return this.verifyIdentity(identity);
}
resolveSkillPackageVersion(
name: string,
version: string,
): ResolvedSkillPackageIdentity {
return this.resolveVersion(name, version);
}
resolveVersion(name: string, version: string): ResolvedSkillPackageIdentity {
const normalizedName = normalizeLookupName(name);
const normalizedVersion = normalizeLookupVersion(version);
const registry = this.loadRegistry();
const matches = registry.packages.filter(
(candidate) =>
candidate.name === normalizedName &&
candidate.version === normalizedVersion,
);
if (matches.length !== 1) {
return fail(
`Expected one registry identity for ${normalizedName}@${normalizedVersion}, found ${matches.length}`,
);
}
const [identity] = matches;
if (identity.status === "blocked") {
return fail(
`Skill package ${normalizedName}@${normalizedVersion} is blocked`,
);
}
return this.verifyIdentity(identity);
}
verifyAllActiveSkillPackages(): readonly ResolvedSkillPackageIdentity[] {
return this.verifyAllActive();
}
verifyAllActive(): readonly ResolvedSkillPackageIdentity[] {
const registry = this.loadRegistry();
return Object.freeze(
registry.packages
.filter((identity) => identity.status === "active")
.map((identity) => this.verifyIdentity(identity)),
);
}
private loadRegistry(): SkillPackageRegistryDocument {
const currentRegistryPath = resolveExistingPath(
this.registryPath,
"Skill package registry",
);
if (!isInside(this.projectRoot, currentRegistryPath)) {
return fail("Skill package registry escapes the project root");
}
assertRegularFile(currentRegistryPath, "Skill package registry");
let contents: string;
try {
contents = readFileSync(currentRegistryPath, "utf8");
} catch (error) {
return fail(
`Skill package registry cannot be read: ${
error instanceof Error ? error.message : "unknown filesystem error"
}`,
);
}
return parseRegistry(contents);
}
private verifyIdentity(
identity: SkillPackageIdentity,
): ResolvedSkillPackageIdentity {
const requestedPackagePath = resolve(
this.projectRoot,
identity.packagePath,
);
if (!isInside(this.projectRoot, requestedPackagePath)) {
return fail(
`Skill package ${identity.name}@${identity.version} escapes the project root`,
);
}
const packageDirectory = resolveExistingPath(
requestedPackagePath,
`Skill package directory ${identity.packagePath}`,
);
if (!isInside(this.projectRoot, packageDirectory)) {
return fail(
`Skill package ${identity.name}@${identity.version} resolves outside the project root`,
);
}
assertDirectory(
packageDirectory,
`Skill package directory ${identity.packagePath}`,
);
if (packageDirectory !== requestedPackagePath) {
return fail(
`Skill package ${identity.name}@${identity.version} path must not contain symbolic links`,
);
}
const requestedSkillPath = resolve(packageDirectory, "SKILL.md");
const skillPath = resolveExistingPath(
requestedSkillPath,
`SKILL.md for ${identity.name}@${identity.version}`,
);
if (!isInside(this.projectRoot, skillPath)) {
return fail(
`SKILL.md for ${identity.name}@${identity.version} resolves outside the project root`,
);
}
assertRegularFile(
skillPath,
`SKILL.md for ${identity.name}@${identity.version}`,
);
const actualHash = computeSkillPackageSha256(packageDirectory);
if (actualHash !== identity.sha256) {
return fail(
`SHA-256 mismatch for ${identity.name}@${identity.version}: expected ${identity.sha256}, got ${actualHash}`,
);
}
return Object.freeze({ ...identity, resolvedPath: packageDirectory });
}
}
export const LIVE_JYOTISH_SKILL_NAME = "jyotish-vedic-astrology";
export type LiveJyotishSkill = {
name: typeof LIVE_JYOTISH_SKILL_NAME;
version: string;
resolvedPath: string;
};
function parseSkillFrontmatter(raw: string): { name?: string; version?: string } {
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
if (!block) return {};
const name = /^name:\s*["']?([a-z0-9](?:[a-z0-9._-]*[a-z0-9])?)["']?\s*$/im.exec(block[1]);
const version = /^version:\s*["']?([^\s"']+)["']?\s*$/im.exec(block[1]);
return {
...(name ? { name: name[1] } : {}),
...(version ? { version: version[1] } : {}),
};
}
function resolveSkillProjectRoot(options: SkillPackageRegistryOptions = {}): string {
const requestedProjectRoot = resolve(
options.projectRoot ?? process.env.JYOTISHA_PROJECT_ROOT ?? SOURCE_TREE_PROJECT_ROOT,
);
const projectRoot = resolveExistingPath(
requestedProjectRoot,
"Skill registry project root",
);
assertDirectory(projectRoot, "Skill registry project root");
return projectRoot;
}
const LIVE_SKILL_RUNTIME_ENTRIES = [
"SKILL.md",
"references",
"scripts",
"assets",
] as const;
export type LiveJyotishSkillOptions = SkillPackageRegistryOptions & {
/** Overrides `JYOTISH_SKILL_PATH` when tests or callers pass an explicit tree. */
skillPath?: string;
};
/**
* The natal consult skill is the live tree the operator updates by hand.
* It is not a hashed registry package: copy SKILL.md / references / scripts
* and restart, without bumping a version or recomputing sha256.
*/
export function resolveLiveJyotishSkill(
options: LiveJyotishSkillOptions = {},
): LiveJyotishSkill {
const projectRoot = resolveSkillProjectRoot(options);
const configured = options.skillPath?.trim() || process.env.JYOTISH_SKILL_PATH?.trim();
const requested = configured
? (isAbsolute(configured) ? resolve(configured) : resolve(projectRoot, configured))
: resolve(projectRoot, "skills", LIVE_JYOTISH_SKILL_NAME);
if (!isInside(projectRoot, requested)) {
let realRequested: string;
try {
realRequested = realpathSync(requested);
} catch {
fail("Live Jyotish skill path escapes the project root");
}
if (!isInside(projectRoot, realRequested)) {
fail("Live Jyotish skill path escapes the project root");
}
}
const resolvedPath = resolveExistingPath(requested, "Live Jyotish skill directory");
if (!isInside(projectRoot, resolvedPath)) {
fail("Live Jyotish skill directory resolves outside the project root");
}
assertDirectory(resolvedPath, "Live Jyotish skill directory");
if (basename(resolvedPath) !== LIVE_JYOTISH_SKILL_NAME) {
fail(`Live Jyotish skill directory must be named ${LIVE_JYOTISH_SKILL_NAME}`);
}
const skillPath = resolve(resolvedPath, "SKILL.md");
const skillFile = resolveExistingPath(skillPath, `SKILL.md for ${LIVE_JYOTISH_SKILL_NAME}`);
if (!isInside(projectRoot, skillFile)) {
fail(`SKILL.md for ${LIVE_JYOTISH_SKILL_NAME} resolves outside the project root`);
}
let raw: string;
try {
raw = readFileSync(skillFile, "utf8");
} catch (error) {
fail(
`SKILL.md for ${LIVE_JYOTISH_SKILL_NAME} cannot be read: ${
error instanceof Error ? error.message : "unknown filesystem error"
}`,
);
}
const frontmatter = parseSkillFrontmatter(raw);
if (frontmatter.name && frontmatter.name !== LIVE_JYOTISH_SKILL_NAME) {
fail(
`Live Jyotish skill frontmatter name must be ${LIVE_JYOTISH_SKILL_NAME}`,
);
}
for (const name of LIVE_SKILL_RUNTIME_ENTRIES) {
const entry = resolve(resolvedPath, name);
if (!existsSync(entry)) continue;
const real = resolveExistingPath(entry, `Live Jyotish skill ${name}`);
if (!isInside(projectRoot, real)) {
fail(`Live Jyotish skill ${name} resolves outside the project root`);
}
}
return Object.freeze({
name: LIVE_JYOTISH_SKILL_NAME,
version: frontmatter.version || "live",
resolvedPath,
});
}
/**
* Mastra-compatible view of the live consult skill.
*
* The git-tracked live directory also contains leftover hashed
* `versions/` snapshots. Those must not be on the loader path: Mastra walks
* every real subdirectory, and the operator's updates live in the four
* top-level entries (often git-tracked symlinks into the repo root).
*/
export function resolveLiveJyotishSkillRuntimePath(
skill: LiveJyotishSkill,
): string {
const token = createHash("sha256").update(skill.resolvedPath).digest("hex").slice(0, 12);
const runtimeParent = join(RUNTIME_SKILL_ROOT, "live", token);
mkdirSync(runtimeParent, { recursive: true });
const runtimePath = join(runtimeParent, skill.name);
if (basename(runtimePath) !== skill.name) {
fail(`Live runtime skill path must end with ${skill.name}`);
}
mkdirSync(runtimePath, { recursive: true });
for (const name of LIVE_SKILL_RUNTIME_ENTRIES) {
const source = resolve(skill.resolvedPath, name);
if (!existsSync(source)) continue;
const target = resolveExistingPath(source, `Live Jyotish skill ${name}`);
const dest = join(runtimePath, name);
try {
const existing = lstatSync(dest);
if (!existing.isSymbolicLink()) {
fail(`Live runtime entry ${name} is not a symbolic-link alias`);
}
const linked = resolveExistingPath(dest, `Live runtime alias for ${name}`);
if (linked !== target) {
fail(`Live runtime entry ${name} points at an unexpected path`);
}
} catch (error) {
if (error instanceof SkillPackageRegistryError) throw error;
try {
const linkType = statSync(target).isDirectory() ? "dir" : "file";
symlinkSync(target, dest, linkType);
} catch (linkError) {
fail(
`Live runtime alias for ${name} could not be created: ${
linkError instanceof Error ? linkError.message : "unknown filesystem error"
}`,
);
}
}
}
return runtimePath;
}
/**
* Return a Mastra-compatible read-only alias for a verified package.
*
* Mastra validates that the configured skill directory basename matches the
* frontmatter name. Versioned registry packages intentionally live under a
* numeric directory (for example, `versions/10.0.2`), so loading that
* directory directly fails validation. The alias keeps the verified package
* bytes and identity unchanged while exposing the canonical skill name at the
* loader boundary.
*/
export function resolveSkillPackageRuntimePath(
skillPackage: ResolvedSkillPackageIdentity,
): string {
const packageDirectory = resolveExistingPath(
skillPackage.resolvedPath,
`Skill package directory ${skillPackage.packagePath}`,
);
assertDirectory(
packageDirectory,
`Skill package directory ${skillPackage.packagePath}`,
);
const actualHash = computeSkillPackageSha256(packageDirectory);
if (actualHash !== skillPackage.sha256) {
return fail(
`SHA-256 mismatch before runtime loading for ${skillPackage.name}@${skillPackage.version}: expected ${skillPackage.sha256}, got ${actualHash}`,
);
}
const runtimeParent = join(RUNTIME_SKILL_ROOT, skillPackage.sha256);
mkdirSync(runtimeParent, { recursive: true });
const runtimePath = join(runtimeParent, skillPackage.name);
if (basename(runtimePath) !== skillPackage.name) {
return fail(
`Runtime skill path must end with the canonical skill name ${skillPackage.name}`,
);
}
try {
const runtimeEntry = lstatSync(runtimePath);
if (!runtimeEntry.isSymbolicLink()) {
return fail(`Runtime skill path is not a symbolic-link alias: ${runtimePath}`);
}
const linkedTarget = resolveExistingPath(
runtimePath,
`Runtime skill alias for ${skillPackage.name}@${skillPackage.version}`,
);
if (linkedTarget !== packageDirectory) {
return fail(
`Runtime skill alias for ${skillPackage.name} points to an unexpected package`,
);
}
} catch (error) {
if (error instanceof SkillPackageRegistryError) throw error;
try {
symlinkSync(packageDirectory, runtimePath, "dir");
} catch (linkError) {
return fail(
`Runtime skill alias for ${skillPackage.name}@${skillPackage.version} could not be created: ${
linkError instanceof Error ? linkError.message : "unknown filesystem error"
}`,
);
}
}
return runtimePath;
}
export function createSkillPackageRegistry(
options: SkillPackageRegistryOptions = {},
): SkillPackageRegistry {
return new SkillPackageRegistry(options);
}
export function resolveActiveSkillPackage(
name: string,
options?: SkillPackageRegistryOptions,
): ResolvedSkillPackageIdentity {
return createSkillPackageRegistry(options).resolveActiveSkillPackage(name);
}
export function resolveActive(
name: string,
options?: SkillPackageRegistryOptions,
): ResolvedSkillPackageIdentity {
return createSkillPackageRegistry(options).resolveActive(name);
}
export function resolveExactSkillPackage(
name: string,
version: string,
sha256: string,
options?: SkillPackageRegistryOptions,
): ResolvedSkillPackageIdentity {
return createSkillPackageRegistry(options).resolveExactSkillPackage(
name,
version,
sha256,
);
}
export function resolveExact(
name: string,
version: string,
sha256: string,
options?: SkillPackageRegistryOptions,
): ResolvedSkillPackageIdentity {
return createSkillPackageRegistry(options).resolveExact(name, version, sha256);
}
export function resolveSkillPackageVersion(
name: string,
version: string,
options?: SkillPackageRegistryOptions,
): ResolvedSkillPackageIdentity {
return createSkillPackageRegistry(options).resolveSkillPackageVersion(
name,
version,
);
}
export function verifyAllActiveSkillPackages(
options?: SkillPackageRegistryOptions,
): readonly ResolvedSkillPackageIdentity[] {
return createSkillPackageRegistry(options).verifyAllActiveSkillPackages();
}
export function verifyAllActive(
options?: SkillPackageRegistryOptions,
): readonly ResolvedSkillPackageIdentity[] {
return createSkillPackageRegistry(options).verifyAllActive();
}