Merge pull request #4299 from aliou/fix/resource-location-in-config-tui
fix(coding-agent): preserve .agents provenance in skill metadata
This commit is contained in:
@@ -2182,6 +2182,7 @@ export class DefaultPackageManager implements PackageManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Project extensions from .pi/
|
||||||
addResources(
|
addResources(
|
||||||
"extensions",
|
"extensions",
|
||||||
collectAutoExtensionEntries(projectDirs.extensions),
|
collectAutoExtensionEntries(projectDirs.extensions),
|
||||||
@@ -2189,16 +2190,32 @@ export class DefaultPackageManager implements PackageManager {
|
|||||||
projectOverrides.extensions,
|
projectOverrides.extensions,
|
||||||
projectBaseDir,
|
projectBaseDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Project skills from .pi/
|
||||||
addResources(
|
addResources(
|
||||||
"skills",
|
"skills",
|
||||||
[
|
collectAutoSkillEntries(projectDirs.skills, "pi"),
|
||||||
...collectAutoSkillEntries(projectDirs.skills, "pi"),
|
|
||||||
...projectAgentsSkillDirs.flatMap((dir) => collectAutoSkillEntries(dir, "agents")),
|
|
||||||
],
|
|
||||||
projectMetadata,
|
projectMetadata,
|
||||||
projectOverrides.skills,
|
projectOverrides.skills,
|
||||||
projectBaseDir,
|
projectBaseDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Project skills from .agents/ (each with its own baseDir)
|
||||||
|
for (const agentsSkillsDir of projectAgentsSkillDirs) {
|
||||||
|
const agentsBaseDir = dirname(agentsSkillsDir); // the .agents directory
|
||||||
|
const agentsMetadata: PathMetadata = {
|
||||||
|
...projectMetadata,
|
||||||
|
baseDir: agentsBaseDir,
|
||||||
|
};
|
||||||
|
addResources(
|
||||||
|
"skills",
|
||||||
|
collectAutoSkillEntries(agentsSkillsDir, "agents"),
|
||||||
|
agentsMetadata,
|
||||||
|
projectOverrides.skills,
|
||||||
|
agentsBaseDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
addResources(
|
addResources(
|
||||||
"prompts",
|
"prompts",
|
||||||
collectAutoPromptEntries(projectDirs.prompts),
|
collectAutoPromptEntries(projectDirs.prompts),
|
||||||
@@ -2214,6 +2231,7 @@ export class DefaultPackageManager implements PackageManager {
|
|||||||
projectBaseDir,
|
projectBaseDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// User extensions from ~/.pi/agent/
|
||||||
addResources(
|
addResources(
|
||||||
"extensions",
|
"extensions",
|
||||||
collectAutoExtensionEntries(userDirs.extensions),
|
collectAutoExtensionEntries(userDirs.extensions),
|
||||||
@@ -2221,13 +2239,30 @@ export class DefaultPackageManager implements PackageManager {
|
|||||||
userOverrides.extensions,
|
userOverrides.extensions,
|
||||||
globalBaseDir,
|
globalBaseDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// User skills from ~/.pi/agent/
|
||||||
addResources(
|
addResources(
|
||||||
"skills",
|
"skills",
|
||||||
[...collectAutoSkillEntries(userDirs.skills, "pi"), ...collectAutoSkillEntries(userAgentsSkillsDir, "agents")],
|
collectAutoSkillEntries(userDirs.skills, "pi"),
|
||||||
userMetadata,
|
userMetadata,
|
||||||
userOverrides.skills,
|
userOverrides.skills,
|
||||||
globalBaseDir,
|
globalBaseDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// User skills from ~/.agents/ (with its own baseDir)
|
||||||
|
const userAgentsBaseDir = dirname(userAgentsSkillsDir);
|
||||||
|
const userAgentsMetadata: PathMetadata = {
|
||||||
|
...userMetadata,
|
||||||
|
baseDir: userAgentsBaseDir,
|
||||||
|
};
|
||||||
|
addResources(
|
||||||
|
"skills",
|
||||||
|
collectAutoSkillEntries(userAgentsSkillsDir, "agents"),
|
||||||
|
userAgentsMetadata,
|
||||||
|
userOverrides.skills,
|
||||||
|
userAgentsBaseDir,
|
||||||
|
);
|
||||||
|
|
||||||
addResources(
|
addResources(
|
||||||
"prompts",
|
"prompts",
|
||||||
collectAutoPromptEntries(userDirs.prompts),
|
collectAutoPromptEntries(userDirs.prompts),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* TUI component for managing package resources (enable/disable)
|
* TUI component for managing package resources (enable/disable)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { homedir } from "node:os";
|
||||||
import { basename, dirname, join, relative } from "node:path";
|
import { basename, dirname, join, relative } from "node:path";
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
@@ -55,12 +56,34 @@ interface ResourceGroup {
|
|||||||
subgroups: ResourceSubgroup[];
|
subgroups: ResourceSubgroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBaseDir(baseDir: string): string {
|
||||||
|
const homeDir = homedir();
|
||||||
|
let displayPath: string;
|
||||||
|
|
||||||
|
if (baseDir === homeDir) {
|
||||||
|
displayPath = "~";
|
||||||
|
} else if (baseDir.startsWith(homeDir)) {
|
||||||
|
// Replace home prefix with ~, normalize separators for display
|
||||||
|
const rest = baseDir.slice(homeDir.length);
|
||||||
|
displayPath = `~${rest.replace(/\\/g, "/")}`;
|
||||||
|
} else {
|
||||||
|
displayPath = baseDir.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayPath.endsWith("/") ? displayPath : `${displayPath}/`;
|
||||||
|
}
|
||||||
|
|
||||||
function getGroupLabel(metadata: PathMetadata): string {
|
function getGroupLabel(metadata: PathMetadata): string {
|
||||||
if (metadata.origin === "package") {
|
if (metadata.origin === "package") {
|
||||||
return `${metadata.source} (${metadata.scope})`;
|
return `${metadata.source} (${metadata.scope})`;
|
||||||
}
|
}
|
||||||
// Top-level resources
|
// Top-level resources
|
||||||
if (metadata.source === "auto") {
|
if (metadata.source === "auto") {
|
||||||
|
if (metadata.baseDir) {
|
||||||
|
return metadata.scope === "user"
|
||||||
|
? `User (${formatBaseDir(metadata.baseDir)})`
|
||||||
|
: `Project (${formatBaseDir(metadata.baseDir)})`;
|
||||||
|
}
|
||||||
return metadata.scope === "user" ? "User (~/.pi/agent/)" : "Project (.pi/)";
|
return metadata.scope === "user" ? "User (~/.pi/agent/)" : "Project (.pi/)";
|
||||||
}
|
}
|
||||||
return metadata.scope === "user" ? "User settings" : "Project settings";
|
return metadata.scope === "user" ? "User settings" : "Project settings";
|
||||||
@@ -72,7 +95,7 @@ function buildGroups(resolved: ResolvedPaths): ResourceGroup[] {
|
|||||||
const addToGroup = (resources: ResolvedResource[], resourceType: ResourceType) => {
|
const addToGroup = (resources: ResolvedResource[], resourceType: ResourceType) => {
|
||||||
for (const res of resources) {
|
for (const res of resources) {
|
||||||
const { path, enabled, metadata } = res;
|
const { path, enabled, metadata } = res;
|
||||||
const groupKey = `${metadata.origin}:${metadata.scope}:${metadata.source}`;
|
const groupKey = `${metadata.origin}:${metadata.scope}:${metadata.source}:${metadata.baseDir ?? ""}`;
|
||||||
|
|
||||||
if (!groupMap.has(groupKey)) {
|
if (!groupMap.has(groupKey)) {
|
||||||
groupMap.set(groupKey, {
|
groupMap.set(groupKey, {
|
||||||
|
|||||||
@@ -274,6 +274,94 @@ Content`,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("auto-discovered skill metadata", () => {
|
||||||
|
it("should use the agent dir as baseDir for user .pi/agent skills", async () => {
|
||||||
|
const skillPath = join(agentDir, "skills", "user-pi", "SKILL.md");
|
||||||
|
mkdirSync(join(agentDir, "skills", "user-pi"), { recursive: true });
|
||||||
|
writeFileSync(skillPath, "---\nname: user-pi\ndescription: user pi\n---\n");
|
||||||
|
|
||||||
|
const result = await packageManager.resolve();
|
||||||
|
const skill = result.skills.find((r) => r.path === skillPath);
|
||||||
|
|
||||||
|
expect(skill?.metadata.source).toBe("auto");
|
||||||
|
expect(skill?.metadata.scope).toBe("user");
|
||||||
|
expect(skill?.metadata.baseDir).toBe(agentDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use the project .pi dir as baseDir for project .pi skills", async () => {
|
||||||
|
const projectBaseDir = join(tempDir, ".pi");
|
||||||
|
const skillPath = join(projectBaseDir, "skills", "project-pi", "SKILL.md");
|
||||||
|
mkdirSync(join(projectBaseDir, "skills", "project-pi"), { recursive: true });
|
||||||
|
writeFileSync(skillPath, "---\nname: project-pi\ndescription: project pi\n---\n");
|
||||||
|
|
||||||
|
const result = await packageManager.resolve();
|
||||||
|
const skill = result.skills.find((r) => r.path === skillPath);
|
||||||
|
|
||||||
|
expect(skill?.metadata.source).toBe("auto");
|
||||||
|
expect(skill?.metadata.scope).toBe("project");
|
||||||
|
expect(skill?.metadata.baseDir).toBe(projectBaseDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use ~/.agents as baseDir for user .agents skills", async () => {
|
||||||
|
const previousHome = process.env.HOME;
|
||||||
|
process.env.HOME = tempDir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agentsBaseDir = join(tempDir, ".agents");
|
||||||
|
const skillPath = join(agentsBaseDir, "skills", "user-agents", "SKILL.md");
|
||||||
|
mkdirSync(join(agentsBaseDir, "skills", "user-agents"), { recursive: true });
|
||||||
|
writeFileSync(skillPath, "---\nname: user-agents\ndescription: user agents\n---\n");
|
||||||
|
|
||||||
|
const result = await packageManager.resolve();
|
||||||
|
const skill = result.skills.find((r) => r.path === skillPath);
|
||||||
|
|
||||||
|
expect(skill?.metadata.source).toBe("auto");
|
||||||
|
expect(skill?.metadata.scope).toBe("user");
|
||||||
|
expect(skill?.metadata.baseDir).toBe(agentsBaseDir);
|
||||||
|
} finally {
|
||||||
|
if (previousHome === undefined) {
|
||||||
|
delete process.env.HOME;
|
||||||
|
} else {
|
||||||
|
process.env.HOME = previousHome;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use each project .agents dir as baseDir for project .agents skills", async () => {
|
||||||
|
const repoRoot = join(tempDir, "repo");
|
||||||
|
const nestedCwd = join(repoRoot, "packages", "feature");
|
||||||
|
mkdirSync(nestedCwd, { recursive: true });
|
||||||
|
mkdirSync(join(repoRoot, ".git"), { recursive: true });
|
||||||
|
|
||||||
|
const repoAgentsBaseDir = join(repoRoot, ".agents");
|
||||||
|
const repoSkill = join(repoAgentsBaseDir, "skills", "repo", "SKILL.md");
|
||||||
|
mkdirSync(join(repoAgentsBaseDir, "skills", "repo"), { recursive: true });
|
||||||
|
writeFileSync(repoSkill, "---\nname: repo\ndescription: repo\n---\n");
|
||||||
|
|
||||||
|
const packageAgentsBaseDir = join(repoRoot, "packages", ".agents");
|
||||||
|
const packageSkill = join(packageAgentsBaseDir, "skills", "package", "SKILL.md");
|
||||||
|
mkdirSync(join(packageAgentsBaseDir, "skills", "package"), { recursive: true });
|
||||||
|
writeFileSync(packageSkill, "---\nname: package\ndescription: package\n---\n");
|
||||||
|
|
||||||
|
const pm = new DefaultPackageManager({
|
||||||
|
cwd: nestedCwd,
|
||||||
|
agentDir,
|
||||||
|
settingsManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pm.resolve();
|
||||||
|
const resolvedRepoSkill = result.skills.find((r) => r.path === repoSkill);
|
||||||
|
const resolvedPackageSkill = result.skills.find((r) => r.path === packageSkill);
|
||||||
|
|
||||||
|
expect(resolvedRepoSkill?.metadata.source).toBe("auto");
|
||||||
|
expect(resolvedRepoSkill?.metadata.scope).toBe("project");
|
||||||
|
expect(resolvedRepoSkill?.metadata.baseDir).toBe(repoAgentsBaseDir);
|
||||||
|
expect(resolvedPackageSkill?.metadata.source).toBe("auto");
|
||||||
|
expect(resolvedPackageSkill?.metadata.scope).toBe("project");
|
||||||
|
expect(resolvedPackageSkill?.metadata.baseDir).toBe(packageAgentsBaseDir);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe(".agents/skills auto-discovery", () => {
|
describe(".agents/skills auto-discovery", () => {
|
||||||
it("should scan .agents/skills from cwd up to git repo root", async () => {
|
it("should scan .agents/skills from cwd up to git repo root", async () => {
|
||||||
const repoRoot = join(tempDir, "repo");
|
const repoRoot = join(tempDir, "repo");
|
||||||
|
|||||||
Reference in New Issue
Block a user