mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
import { existsSync, readFileSync } from "fs";
|
|
import { homedir } from "os";
|
|
import { isAbsolute, join, relative, resolve, sep } from "path";
|
|
import type { SkillInfo, SkillInstallInfo, SkillInstallScope } from "@/lib/api-types";
|
|
|
|
interface SkillLockEntry {
|
|
source?: unknown;
|
|
sourceType?: unknown;
|
|
skillPath?: unknown;
|
|
ref?: unknown;
|
|
skillFolderHash?: unknown;
|
|
computedHash?: unknown;
|
|
}
|
|
|
|
interface SkillLockFile {
|
|
skills?: Record<string, SkillLockEntry>;
|
|
}
|
|
|
|
interface GlobalLockPathOptions {
|
|
homeDir?: string;
|
|
xdgStateHome?: string;
|
|
}
|
|
|
|
interface AnnotateSkillOptions {
|
|
cwd: string;
|
|
agentDir: string;
|
|
globalSkillRoots?: string[];
|
|
globalLockPath?: string;
|
|
projectLockPath?: string;
|
|
}
|
|
|
|
export function getGlobalSkillsLockPath({
|
|
homeDir = homedir(),
|
|
xdgStateHome = process.env.XDG_STATE_HOME,
|
|
}: GlobalLockPathOptions = {}): string {
|
|
return xdgStateHome
|
|
? join(xdgStateHome, "skills", ".skill-lock.json")
|
|
: join(homeDir, ".agents", ".skill-lock.json");
|
|
}
|
|
|
|
function readSkillLock(path: string): Record<string, SkillLockEntry> {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(path, "utf8")) as SkillLockFile;
|
|
return parsed.skills && typeof parsed.skills === "object" ? parsed.skills : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function isWithin(path: string, root: string): boolean {
|
|
const rel = relative(resolve(root), resolve(path));
|
|
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
}
|
|
|
|
function findLockEntry(
|
|
entries: Record<string, SkillLockEntry>,
|
|
skillName: string,
|
|
): SkillLockEntry | undefined {
|
|
if (entries[skillName]) return entries[skillName];
|
|
const normalizedName = skillName.toLowerCase();
|
|
const key = Object.keys(entries).find((name) => name.toLowerCase() === normalizedName);
|
|
return key ? entries[key] : undefined;
|
|
}
|
|
|
|
function normalizeSource(source: string, sourceType?: string): string {
|
|
if (sourceType !== "github") return source.replace(/\/$/, "");
|
|
return source
|
|
.replace(/^git\+/, "")
|
|
.replace(/^https?:\/\/github\.com\//, "")
|
|
.replace(/^git@github\.com:/, "")
|
|
.replace(/\.git$/, "")
|
|
.replace(/\/$/, "");
|
|
}
|
|
|
|
function buildSkillsShUrl(source: string, skillName: string): string | undefined {
|
|
if (!source || source.includes("://") || source.startsWith("git@")) return undefined;
|
|
const sourcePath = source
|
|
.split("/")
|
|
.filter(Boolean)
|
|
.map(encodeURIComponent)
|
|
.join("/");
|
|
if (!sourcePath) return undefined;
|
|
return `https://skills.sh/${sourcePath}/${encodeURIComponent(skillName)}`;
|
|
}
|
|
|
|
function getInstallInfo(
|
|
entries: Record<string, SkillLockEntry>,
|
|
skillName: string,
|
|
scope: SkillInstallScope,
|
|
): SkillInstallInfo | undefined {
|
|
const entry = findLockEntry(entries, skillName);
|
|
if (!entry || typeof entry.source !== "string" || !entry.source.trim()) return undefined;
|
|
|
|
const sourceType = typeof entry.sourceType === "string" ? entry.sourceType : undefined;
|
|
const source = normalizeSource(entry.source.trim(), sourceType);
|
|
if (!source) return undefined;
|
|
const skillPath = typeof entry.skillPath === "string" ? entry.skillPath : undefined;
|
|
const ref = typeof entry.ref === "string" ? entry.ref : undefined;
|
|
const rawVersionHash = scope === "global" ? entry.skillFolderHash : entry.computedHash;
|
|
const versionHash = typeof rawVersionHash === "string" && rawVersionHash
|
|
? rawVersionHash
|
|
: undefined;
|
|
const isGitHubSource =
|
|
sourceType === "github" && /^[\w.-]+\/[\w.-]+$/.test(source);
|
|
const hasComparableVersion = scope === "global" || !ref;
|
|
|
|
return {
|
|
package: `${source}@${skillName}`,
|
|
scope,
|
|
source,
|
|
sourceType,
|
|
skillsShUrl: sourceType === "local" ? undefined : buildSkillsShUrl(source, skillName),
|
|
...(skillPath && { skillPath }),
|
|
...(ref && { ref }),
|
|
...(versionHash && { versionHash }),
|
|
canCheckForUpdates: Boolean(
|
|
isGitHubSource && skillPath && versionHash && hasComparableVersion,
|
|
),
|
|
};
|
|
}
|
|
|
|
export function annotateSkillsWithInstallInfo(
|
|
skills: SkillInfo[],
|
|
{
|
|
cwd,
|
|
agentDir,
|
|
globalSkillRoots = [join(agentDir, "skills")],
|
|
globalLockPath = getGlobalSkillsLockPath(),
|
|
projectLockPath = join(cwd, "skills-lock.json"),
|
|
}: AnnotateSkillOptions,
|
|
): SkillInfo[] {
|
|
const globalEntries = readSkillLock(globalLockPath);
|
|
const projectEntries = readSkillLock(projectLockPath);
|
|
const projectSkillsRoot = join(cwd, ".pi", "skills");
|
|
|
|
return skills.map((skill) => {
|
|
if (!existsSync(skill.filePath)) return skill;
|
|
|
|
const install = globalSkillRoots.some((root) => isWithin(skill.filePath, root))
|
|
? getInstallInfo(globalEntries, skill.name, "global")
|
|
: isWithin(skill.filePath, projectSkillsRoot)
|
|
? getInstallInfo(projectEntries, skill.name, "project")
|
|
: undefined;
|
|
|
|
return install ? { ...skill, install } : skill;
|
|
});
|
|
}
|