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
@@ -3,8 +3,11 @@ import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
import type { ResolvedPaths } from "../src/core/package-manager.ts";
import { InMemorySettingsStorage, SettingsManager } from "../src/core/settings-manager.ts";
import { ProjectTrustStore } from "../src/core/trust-manager.ts";
import { main } from "../src/main.ts";
import { ConfigSelectorComponent } from "../src/modes/interactive/components/config-selector.ts";
import { handlePackageCommand } from "../src/package-manager-cli.ts";
describe("package commands", () => {
@@ -28,6 +31,24 @@ describe("package commands", () => {
expect(await handlePackageCommand(args)).toBe(true);
}
function extensionPaths(
packageRoot: string,
source: string,
scope: "user" | "project",
names: string[],
): ResolvedPaths {
return {
extensions: names.map((name) => ({
path: join(packageRoot, "extensions", name),
enabled: true,
metadata: { source, scope, origin: "package", baseDir: packageRoot },
})),
skills: [],
prompts: [],
themes: [],
};
}
beforeEach(() => {
tempDir = join(tmpdir(), `pi-package-commands-${Date.now()}-${Math.random().toString(36).slice(2)}`);
agentDir = join(tempDir, "agent");
@@ -350,6 +371,37 @@ describe("package commands", () => {
}
});
it("cycles project package overrides in config local mode", async () => {
const storage = new InMemorySettingsStorage();
storage.withLock("global", () => JSON.stringify({ packages: ["npm:pi-tools"] }));
const settingsManager = SettingsManager.fromStorage(storage, { projectTrusted: true });
const resolvedPaths = extensionPaths(join(tempDir, "pkg"), "npm:pi-tools", "user", ["bar.ts"]);
const selector = new ConfigSelectorComponent(
{ global: resolvedPaths, project: resolvedPaths },
settingsManager,
projectDir,
agentDir,
() => {},
() => {},
() => {},
24,
"project",
);
selector.getResourceList().handleInput(" ");
expect(settingsManager.getProjectSettings().packages).toEqual([
{ source: "npm:pi-tools", autoload: false, extensions: ["-extensions/bar.ts"] },
]);
selector.getResourceList().handleInput(" ");
expect(settingsManager.getProjectSettings().packages).toEqual([
{ source: "npm:pi-tools", autoload: false, extensions: ["+extensions/bar.ts"] },
]);
selector.getResourceList().handleInput(" ");
expect(settingsManager.getProjectSettings().packages).toEqual([]);
});
it("shows a friendly error for unknown install options", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
@@ -1674,6 +1674,49 @@ Content`,
expect(result.extensions.some((r) => isEnabled(r, "one.ts"))).toBe(true);
expect(result.extensions.some((r) => isDisabled(r, "two.ts"))).toBe(true);
});
it("should resolve autoload-disabled project package entries as deltas over global packages", async () => {
const pkgDir = join(agentDir, "npm", "node_modules", "pi-tools");
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name: "pi-tools", version: "1.0.0" }));
writeFileSync(join(pkgDir, "extensions", "foo.ts"), "export default function() {}");
writeFileSync(join(pkgDir, "extensions", "bar.ts"), "export default function() {}");
settingsManager.setPackages(["npm:pi-tools"]);
settingsManager.setProjectPackages([
{ source: "npm:pi-tools", autoload: false, extensions: ["-extensions/foo.ts"] },
]);
const runCommandSpy = vi
.spyOn(packageManager as unknown as PackageManagerInternals, "runCommand")
.mockRejectedValue(new Error("unexpected install"));
const result = await packageManager.resolve();
const states = Object.fromEntries(
result.extensions.map((resource) => [
resource.path,
{ enabled: resource.enabled, scope: resource.metadata.scope },
]),
);
expect(runCommandSpy).not.toHaveBeenCalled();
expect(states[join(pkgDir, "extensions", "foo.ts")]).toEqual({ enabled: false, scope: "project" });
expect(states[join(pkgDir, "extensions", "bar.ts")]).toEqual({ enabled: true, scope: "user" });
});
it("should resolve autoload-disabled package entries as positive-only without a global package", async () => {
const pkgDir = join(tempDir, "positive-only-pkg");
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
mkdirSync(join(pkgDir, "skills", "foo"), { recursive: true });
writeFileSync(join(pkgDir, "extensions", "foo.ts"), "export default function() {}");
writeFileSync(join(pkgDir, "extensions", "bar.ts"), "export default function() {}");
writeFileSync(join(pkgDir, "skills", "foo", "SKILL.md"), "# Foo\n");
settingsManager.setProjectPackages([
{ source: relative(join(tempDir, ".pi"), pkgDir), autoload: false, extensions: ["+extensions/foo.ts"] },
]);
const result = await packageManager.resolve();
expect(result.extensions.map((resource) => resource.path)).toEqual([join(pkgDir, "extensions", "foo.ts")]);
expect(result.skills).toEqual([]);
});
});
describe("force-include patterns", () => {