mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
feat: integrate Pi backend and Pi Web
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user