723 lines
21 KiB
TypeScript
723 lines
21 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 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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/6.9.14`), 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();
|
|
}
|