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).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 { 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 { 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); }