feat(sproutclaw): modularize webui, extensions, and agent config layout
Some checks failed
CI / build-check-test (push) Has been cancelled
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>
This commit is contained in:
102
.pi/agent/extensions/webui/backend/settings/extensions-paths.ts
Normal file
102
.pi/agent/extensions/webui/backend/settings/extensions-paths.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user