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>
204 lines
6.6 KiB
TypeScript
204 lines
6.6 KiB
TypeScript
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
import {
|
|
DefaultPackageManager,
|
|
type PathMetadata,
|
|
type ResolvedResource,
|
|
} from "../../../../../../packages/coding-agent/src/core/package-manager.ts";
|
|
import { SettingsManager, type PackageSource } from "../../../../../../packages/coding-agent/src/core/settings-manager.ts";
|
|
|
|
export interface ExtensionSettingsEntry {
|
|
path: string;
|
|
resolvedPath: string;
|
|
enabled: boolean;
|
|
name: string;
|
|
scope: string;
|
|
source: string;
|
|
}
|
|
|
|
interface ExtensionResourceItem {
|
|
path: string;
|
|
enabled: boolean;
|
|
metadata: PathMetadata;
|
|
}
|
|
|
|
function createManagers(repoRoot: string, agentDir: string) {
|
|
const settingsManager = SettingsManager.create(repoRoot, agentDir);
|
|
const packageManager = new DefaultPackageManager({
|
|
cwd: repoRoot,
|
|
agentDir,
|
|
settingsManager,
|
|
});
|
|
return { settingsManager, packageManager };
|
|
}
|
|
|
|
function normalizeExtensionPath(pathValue: string): string {
|
|
return resolve(pathValue);
|
|
}
|
|
|
|
function readExtensionDisplayName(pathValue: string): string {
|
|
const normalized = pathValue.replace(/\\/g, "/");
|
|
const parts = normalized.split("/").filter(Boolean);
|
|
const file = parts[parts.length - 1] || normalized;
|
|
if (/^index\.[cm]?[tj]s$/i.test(file) && parts.length >= 2) {
|
|
return parts[parts.length - 2].replace(/\.[cm]?[tj]s$/i, "");
|
|
}
|
|
return basename(file).replace(/\.[cm]?[tj]s$/i, "");
|
|
}
|
|
|
|
function toResourceItem(resource: ResolvedResource): ExtensionResourceItem {
|
|
return {
|
|
path: resource.path,
|
|
enabled: resource.enabled,
|
|
metadata: resource.metadata,
|
|
};
|
|
}
|
|
|
|
function getTopLevelBaseDir(scope: "user" | "project", repoRoot: string, agentDir: string): string {
|
|
return scope === "project" ? join(repoRoot, ".pi") : agentDir;
|
|
}
|
|
|
|
function getResourcePattern(item: ExtensionResourceItem, repoRoot: string, agentDir: string): string {
|
|
if (item.metadata.origin === "package") {
|
|
const baseDir = item.metadata.baseDir ?? dirname(item.path);
|
|
return relative(baseDir, item.path);
|
|
}
|
|
const scope = item.metadata.scope as "user" | "project";
|
|
const baseDir = getTopLevelBaseDir(scope, repoRoot, agentDir);
|
|
return relative(baseDir, item.path);
|
|
}
|
|
|
|
function applyPatternUpdate(current: string[], pattern: string, enabled: boolean): string[] {
|
|
const disablePattern = `-${pattern}`;
|
|
const enablePattern = `+${pattern}`;
|
|
const updated = current.filter((entry) => {
|
|
const stripped = entry.startsWith("!") || entry.startsWith("+") || entry.startsWith("-") ? entry.slice(1) : entry;
|
|
return stripped !== pattern;
|
|
});
|
|
updated.push(enabled ? enablePattern : disablePattern);
|
|
return updated;
|
|
}
|
|
|
|
function toggleTopLevelResource(
|
|
item: ExtensionResourceItem,
|
|
enabled: boolean,
|
|
settingsManager: SettingsManager,
|
|
repoRoot: string,
|
|
agentDir: string,
|
|
): void {
|
|
const scope = item.metadata.scope as "user" | "project";
|
|
const settings =
|
|
scope === "project" ? settingsManager.getProjectSettings() : settingsManager.getGlobalSettings();
|
|
const current = [...(settings.extensions ?? [])];
|
|
const pattern = getResourcePattern(item, repoRoot, agentDir);
|
|
const updated = applyPatternUpdate(current, pattern, enabled);
|
|
|
|
if (scope === "project") {
|
|
settingsManager.setProjectExtensionPaths(updated);
|
|
} else {
|
|
settingsManager.setExtensionPaths(updated);
|
|
}
|
|
}
|
|
|
|
function togglePackageResource(
|
|
item: ExtensionResourceItem,
|
|
enabled: boolean,
|
|
settingsManager: SettingsManager,
|
|
repoRoot: string,
|
|
agentDir: string,
|
|
): void {
|
|
const scope = item.metadata.scope as "user" | "project";
|
|
const settings =
|
|
scope === "project" ? settingsManager.getProjectSettings() : settingsManager.getGlobalSettings();
|
|
const packages = [...(settings.packages ?? [])] as PackageSource[];
|
|
const pkgIndex = packages.findIndex((pkg) => {
|
|
const source = typeof pkg === "string" ? pkg : pkg.source;
|
|
return source === item.metadata.source;
|
|
});
|
|
if (pkgIndex === -1) return;
|
|
|
|
let pkg = packages[pkgIndex];
|
|
if (typeof pkg === "string") {
|
|
pkg = { source: pkg };
|
|
packages[pkgIndex] = pkg;
|
|
}
|
|
|
|
const current = [...((pkg.extensions ?? []) as string[])];
|
|
const pattern = getResourcePattern(item, repoRoot, agentDir);
|
|
const updated = applyPatternUpdate(current, pattern, enabled);
|
|
(pkg as Record<string, unknown>).extensions = updated.length > 0 ? updated : undefined;
|
|
|
|
if (!pkg.skills && !pkg.extensions && !pkg.prompts && !pkg.themes) {
|
|
packages.splice(pkgIndex, 1);
|
|
}
|
|
|
|
if (scope === "project") {
|
|
settingsManager.setProjectPackages(packages);
|
|
} else {
|
|
settingsManager.setPackages(packages);
|
|
}
|
|
}
|
|
|
|
function toggleExtensionResource(
|
|
item: ExtensionResourceItem,
|
|
enabled: boolean,
|
|
settingsManager: SettingsManager,
|
|
repoRoot: string,
|
|
agentDir: string,
|
|
): void {
|
|
if (item.metadata.origin === "top-level") {
|
|
toggleTopLevelResource(item, enabled, settingsManager, repoRoot, agentDir);
|
|
} else {
|
|
togglePackageResource(item, enabled, settingsManager, repoRoot, agentDir);
|
|
}
|
|
}
|
|
|
|
function pathsMatch(a: string, b: string): boolean {
|
|
const left = normalizeExtensionPath(a);
|
|
const right = normalizeExtensionPath(b);
|
|
if (left === right) return true;
|
|
return resolve(a) === resolve(b);
|
|
}
|
|
|
|
function mapExtensionEntry(resource: ResolvedResource): ExtensionSettingsEntry {
|
|
return {
|
|
path: normalizeExtensionPath(resource.path),
|
|
resolvedPath: normalizeExtensionPath(resource.path),
|
|
enabled: resource.enabled,
|
|
name: readExtensionDisplayName(resource.path),
|
|
scope: resource.metadata.scope || "",
|
|
source: resource.metadata.source || "",
|
|
};
|
|
}
|
|
|
|
export async function listExtensionSettings(
|
|
repoRoot: string,
|
|
agentDir: string,
|
|
): Promise<ExtensionSettingsEntry[]> {
|
|
const { packageManager } = createManagers(repoRoot, agentDir);
|
|
const resolved = await packageManager.resolve(async () => "skip");
|
|
return resolved.extensions.map(mapExtensionEntry).sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
export async function setExtensionEnabled(
|
|
repoRoot: string,
|
|
agentDir: string,
|
|
pathValue: string,
|
|
enabled: boolean,
|
|
): Promise<ExtensionSettingsEntry> {
|
|
const { settingsManager, packageManager } = createManagers(repoRoot, agentDir);
|
|
const resolved = await packageManager.resolve(async () => "skip");
|
|
const match = resolved.extensions.find((resource) => pathsMatch(resource.path, pathValue));
|
|
if (!match) {
|
|
throw new Error("未找到对应扩展");
|
|
}
|
|
|
|
toggleExtensionResource(toResourceItem(match), enabled, settingsManager, repoRoot, agentDir);
|
|
|
|
const refreshed = await packageManager.resolve(async () => "skip");
|
|
const updated = refreshed.extensions.find((resource) => pathsMatch(resource.path, pathValue));
|
|
if (!updated) {
|
|
throw new Error("更新扩展状态后未能重新解析");
|
|
}
|
|
return mapExtensionEntry(updated);
|
|
}
|