Improve project-local pi config (#6309)

* feat(coding-agent): improve config resource overrides

* fix(coding-agent): simplify config resource overrides
This commit is contained in:
Armin Ronacher
2026-07-06 20:50:30 +02:00
committed by GitHub
parent 8c0ccd14b3
commit c8ada4e76e
9 changed files with 599 additions and 81 deletions
@@ -188,6 +188,7 @@ function resourcePrecedenceRank(m: PathMetadata): number {
}
interface PackageFilter {
autoload?: boolean;
extensions?: string[];
skills?: string[];
prompts?: string[];
@@ -772,6 +773,25 @@ function applyPatterns(allPaths: string[], patterns: string[], baseDir: string):
return new Set(result);
}
function applyAutoloadDisabledPatterns(allPaths: string[], patterns: string[], baseDir: string): Map<string, boolean> {
const result = new Map<string, boolean>();
for (const pattern of patterns) {
const target = pattern.slice(
pattern.startsWith("+") || pattern.startsWith("-") || pattern.startsWith("!") ? 1 : 0,
);
const enabled = !pattern.startsWith("-") && !pattern.startsWith("!");
const exact = pattern.startsWith("+") || pattern.startsWith("-");
for (const filePath of allPaths) {
if (
exact ? matchesAnyExactPattern(filePath, [target], baseDir) : matchesAnyPattern(filePath, [target], baseDir)
) {
result.set(filePath, enabled);
}
}
}
return result;
}
export class DefaultPackageManager implements PackageManager {
private cwd: string;
private agentDir: string;
@@ -1225,38 +1245,39 @@ export class DefaultPackageManager implements PackageManager {
for (const { pkg, scope } of sources) {
const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
const filter = typeof pkg === "object" ? pkg : undefined;
const parsed = this.parseSource(sourceStr);
const deltaBase = this.findAutoloadDeltaBase(pkg, scope, sources);
const resolvedSource = deltaBase?.source ?? sourceStr;
const resolvedScope = deltaBase?.scope ?? scope;
const parsed = this.parseSource(resolvedSource);
const metadata: PathMetadata = { source: sourceStr, scope, origin: "package" };
if (parsed.type === "local") {
const baseDir = this.getBaseDirForScope(scope);
const baseDir = this.getBaseDirForScope(resolvedScope);
this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);
continue;
}
const installMissing = async (): Promise<boolean> => {
if (isOfflineModeEnabled()) {
return false;
}
if (isOfflineModeEnabled()) return false;
if (!onMissing) {
await this.installParsedSource(parsed, scope);
await this.installParsedSource(parsed, resolvedScope);
return true;
}
const action = await onMissing(sourceStr);
const action = await onMissing(resolvedSource);
if (action === "skip") return false;
if (action === "error") throw new Error(`Missing source: ${sourceStr}`);
await this.installParsedSource(parsed, scope);
if (action === "error") throw new Error(`Missing source: ${resolvedSource}`);
await this.installParsedSource(parsed, resolvedScope);
return true;
};
if (parsed.type === "npm") {
let installedPath = this.getNpmInstallPath(parsed, scope);
let installedPath = this.getNpmInstallPath(parsed, resolvedScope);
const needsInstall =
!existsSync(installedPath) || !(await this.installedNpmMatchesConfiguredVersion(parsed, installedPath));
if (needsInstall) {
const installed = await installMissing();
if (!installed) continue;
installedPath = this.getNpmInstallPath(parsed, scope);
installedPath = this.getNpmInstallPath(parsed, resolvedScope);
}
metadata.baseDir = installedPath;
this.collectPackageResources(installedPath, accumulator, filter, metadata);
@@ -1264,12 +1285,12 @@ export class DefaultPackageManager implements PackageManager {
}
if (parsed.type === "git") {
const installedPath = this.getGitInstallPath(parsed, scope);
const installedPath = this.getGitInstallPath(parsed, resolvedScope);
if (!existsSync(installedPath)) {
const installed = await installMissing();
if (!installed) continue;
} else if (scope === "temporary" && !parsed.pinned && !isOfflineModeEnabled()) {
await this.refreshTemporaryGitSource(parsed, sourceStr);
} else if (resolvedScope === "temporary" && !parsed.pinned && !isOfflineModeEnabled()) {
await this.refreshTemporaryGitSource(parsed, resolvedSource);
}
metadata.baseDir = installedPath;
this.collectPackageResources(installedPath, accumulator, filter, metadata);
@@ -1277,6 +1298,21 @@ export class DefaultPackageManager implements PackageManager {
}
}
private findAutoloadDeltaBase(
pkg: PackageSource,
scope: SourceScope,
sources: Array<{ pkg: PackageSource; scope: SourceScope }>,
): { source: string; scope: SourceScope } | undefined {
if (scope !== "project" || typeof pkg !== "object" || pkg.autoload !== false) return undefined;
const identity = this.getPackageIdentity(pkg.source, scope);
const userEntry = sources.find(
(entry) =>
entry.scope === "user" &&
this.getPackageIdentity(this.getPackageSourceString(entry.pkg), "user") === identity,
);
return userEntry ? { source: this.getPackageSourceString(userEntry.pkg), scope: "user" } : undefined;
}
private resolveLocalExtensionSource(
source: LocalSource,
accumulator: ResourceAccumulator,
@@ -1655,29 +1691,30 @@ export class DefaultPackageManager implements PackageManager {
/**
* Dedupe packages: if same package identity appears in both global and project,
* keep only the project one (project wins).
* keep only the project one (project wins). A project entry with autoload=false
* is a delta over the global entry, so both are kept (delta first).
*/
private dedupePackages(
packages: Array<{ pkg: PackageSource; scope: SourceScope }>,
): Array<{ pkg: PackageSource; scope: SourceScope }> {
const seen = new Map<string, { pkg: PackageSource; scope: SourceScope }>();
const result: Array<{ pkg: PackageSource; scope: SourceScope }> = [];
const seen = new Map<string, number>();
for (const entry of packages) {
const sourceStr = typeof entry.pkg === "string" ? entry.pkg : entry.pkg.source;
const identity = this.getPackageIdentity(sourceStr, entry.scope);
const existing = seen.get(identity);
if (!existing) {
seen.set(identity, entry);
} else if (entry.scope === "project" && existing.scope === "user") {
// Project wins over user
seen.set(identity, entry);
const identity = this.getPackageIdentity(this.getPackageSourceString(entry.pkg), entry.scope);
const index = seen.get(identity);
if (index === undefined) {
seen.set(identity, result.length);
result.push(entry);
continue;
}
const existing = result[index];
if (existing?.scope === "project" && entry.scope === "user") {
if (typeof existing.pkg === "object" && existing.pkg.autoload === false) result.push(entry);
} else if (entry.scope === "project") {
result[index] = entry;
}
// If existing is project and new is global, keep existing (project)
// If both are same scope, keep first one
}
return Array.from(seen.values());
return result;
}
private parseNpmSpec(spec: string): { name: string; version?: string } {
@@ -2047,9 +2084,11 @@ export class DefaultPackageManager implements PackageManager {
): boolean {
if (filter) {
for (const resourceType of RESOURCE_TYPES) {
const patterns = filter[resourceType as keyof PackageFilter];
const patterns = filter[resourceType];
const target = this.getTargetMap(accumulator, resourceType);
if (patterns !== undefined) {
if (filter.autoload === false) {
this.applyPackageDeltaFilter(packageRoot, patterns ?? [], resourceType, target, metadata);
} else if (patterns !== undefined) {
this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata);
} else {
this.collectDefaultResources(packageRoot, resourceType, target, metadata);
@@ -2136,6 +2175,24 @@ export class DefaultPackageManager implements PackageManager {
}
}
private applyPackageDeltaFilter(
packageRoot: string,
userPatterns: string[],
resourceType: ResourceType,
target: Map<string, { metadata: PathMetadata; enabled: boolean }>,
metadata: PathMetadata,
): void {
if (userPatterns.length === 0) {
return;
}
const { allFiles } = this.collectManifestFiles(packageRoot, resourceType);
const enabledByUser = applyAutoloadDisabledPatterns(allFiles, userPatterns, packageRoot);
for (const [filePath, enabled] of enabledByUser) {
this.addResource(target, filePath, metadata, enabled);
}
}
/**
* Collect all files from a package for a resource type, applying manifest patterns.
* Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files
@@ -66,11 +66,13 @@ export type TransportSetting = Transport;
* Package source for npm/git packages.
* - String form: load all resources from the package
* - Object form: filter which resources to load
* - autoload=false: start empty and only apply explicit resource patterns
*/
export type PackageSource =
| string
| {
source: string;
autoload?: boolean;
extensions?: string[];
skills?: string[];
prompts?: string[];