Some checks failed
CI / build-check-test (push) Has been cancelled
Restructure local extensions into per-feature directories, split WebUI into backend modules with slash commands and systemd support, and track prompts/skills under .pi/agent for portable Gitea deployment. Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import { join, relative, resolve, sep } from "node:path";
|
|
|
|
export function readConfiguredNpmPackageNames(settingsPath: string): Set<string> {
|
|
const names = new Set<string>();
|
|
if (!existsSync(settingsPath)) return names;
|
|
|
|
try {
|
|
const raw = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
const packages = raw?.packages;
|
|
if (!Array.isArray(packages)) return names;
|
|
|
|
for (const entry of packages) {
|
|
const source = typeof entry === "string" ? entry : entry?.source;
|
|
if (typeof source === "string" && source.startsWith("npm:")) {
|
|
const name = source.slice("npm:".length).trim();
|
|
if (name) names.add(name);
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore invalid settings */
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
export function extractNpmPackageNameFromPath(nodeModulesRoot: string, pathValue: string): string | null {
|
|
if (!pathValue) return null;
|
|
|
|
const root = resolve(nodeModulesRoot);
|
|
const normalized = resolve(pathValue);
|
|
if (normalized !== root && !normalized.startsWith(`${root}${sep}`)) {
|
|
return null;
|
|
}
|
|
|
|
const rel = relative(root, normalized);
|
|
const parts = rel.split(sep).filter(Boolean);
|
|
if (parts.length === 0) return null;
|
|
if (parts[0].startsWith("@") && parts.length >= 2) {
|
|
return `${parts[0]}/${parts[1]}`;
|
|
}
|
|
return parts[0] || null;
|
|
}
|
|
|
|
export function isLocalAgentExtension(pathValue: string, agentExtensionsDir: string): boolean {
|
|
if (!pathValue) return false;
|
|
const resolved = resolve(pathValue);
|
|
const root = resolve(agentExtensionsDir);
|
|
return resolved === root || resolved.startsWith(`${root}${sep}`);
|
|
}
|
|
|
|
export function isConfiguredNpmExtension(
|
|
pathValue: string,
|
|
nodeModulesRoot: string,
|
|
configuredPackages: Set<string>,
|
|
): boolean {
|
|
const packageName = extractNpmPackageNameFromPath(nodeModulesRoot, pathValue);
|
|
return packageName !== null && configuredPackages.has(packageName);
|
|
}
|
|
|
|
export function getExtensionCategoryFromPath(
|
|
pathValue: string,
|
|
agentExtensionsDir: string,
|
|
nodeModulesRoot: string,
|
|
configuredPackages: Set<string>,
|
|
): "local" | "npm" | null {
|
|
if (isConfiguredNpmExtension(pathValue, nodeModulesRoot, configuredPackages)) {
|
|
return "npm";
|
|
}
|
|
if (isLocalAgentExtension(pathValue, agentExtensionsDir)) {
|
|
return "local";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolveNpmSource(pathValue: string, nodeModulesRoot: string, fallbackSource: string): string {
|
|
if (fallbackSource.startsWith("npm:")) return fallbackSource;
|
|
const packageName = extractNpmPackageNameFromPath(nodeModulesRoot, pathValue);
|
|
return packageName ? `npm:${packageName}` : fallbackSource;
|
|
}
|
|
|
|
export function readNpmPackageVersion(
|
|
nodeModulesRoot: string,
|
|
pathValue: string,
|
|
source: string,
|
|
): string | undefined {
|
|
let packageName = extractNpmPackageNameFromPath(nodeModulesRoot, pathValue);
|
|
if (!packageName && source.startsWith("npm:")) {
|
|
packageName = source.slice("npm:".length).trim() || null;
|
|
}
|
|
if (!packageName) return undefined;
|
|
|
|
const pkgJsonPath = join(nodeModulesRoot, packageName, "package.json");
|
|
if (!existsSync(pkgJsonPath)) return undefined;
|
|
|
|
try {
|
|
const raw = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { version?: unknown };
|
|
return typeof raw.version === "string" && raw.version.trim() ? raw.version.trim() : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|