Files
sproutclaw/.pi/agent/extensions/webui/backend/settings/extensions-paths.ts
root cf5edd6394
Some checks failed
CI / build-check-test (push) Has been cancelled
feat(sproutclaw): modularize webui, extensions, and agent config layout
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>
2026-06-10 16:57:08 +08:00

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;
}
}