Fix local skill import inventory

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-06 17:28:23 +00:00
1 parent 139cdebe51
commit 5d01b75c60
2 files changed
+78 -2

No files matched your search

@@ -53,6 +53,26 @@ describeEmbeddedPostgres("companySkillService.list", () => {
await tempDb?.cleanup();
});
async function createCompany(companyId = randomUUID()) {
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
return companyId;
}
async function writeSkillTree(skillDir: string, name: string) {
await fs.mkdir(path.join(skillDir, "references"), { recursive: true });
await fs.mkdir(path.join(skillDir, "scripts"), { recursive: true });
await fs.mkdir(path.join(skillDir, "assets"), { recursive: true });
await fs.writeFile(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\n---\n\n# ${name}\n`, "utf8");
await fs.writeFile(path.join(skillDir, "references", "how-to-guide-template.md"), "# Guide Template\n", "utf8");
await fs.writeFile(path.join(skillDir, "scripts", "bootstrap.sh"), "echo ok\n", "utf8");
await fs.writeFile(path.join(skillDir, "assets", "logo.svg"), "<svg />\n", "utf8");
}
it("lists skills without exposing markdown content", async () => {
const companyId = randomUUID();
const skillId = randomUUID();
@@ -101,6 +121,55 @@ describeEmbeddedPostgres("companySkillService.list", () => {
});
});
it("indexes nested files when importing a direct local skill directory", async () => {
const companyId = await createCompany();
const skillDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-direct-skill-"));
cleanupDirs.add(skillDir);
await writeSkillTree(skillDir, "Diataxis");
const result = await svc.importFromSource(companyId, skillDir);
const imported = result.imported[0]!;
const inventoryPaths = imported.fileInventory.map((entry) => entry.path);
expect(inventoryPaths).toEqual([
"assets/logo.svg",
"references/how-to-guide-template.md",
"scripts/bootstrap.sh",
"SKILL.md",
]);
expect(imported.fileInventory).toEqual(expect.arrayContaining([
{ path: "references/how-to-guide-template.md", kind: "reference" },
{ path: "scripts/bootstrap.sh", kind: "script" },
{ path: "assets/logo.svg", kind: "asset" },
]));
const reference = await svc.readFile(companyId, imported.id, "references/how-to-guide-template.md");
expect(reference).toMatchObject({
path: "references/how-to-guide-template.md",
content: "# Guide Template\n",
});
});
it("keeps parent-directory local imports scoped to the requested skill", async () => {
const companyId = await createCompany();
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-parent-skills-"));
cleanupDirs.add(rootDir);
await writeSkillTree(path.join(rootDir, "diataxis"), "Diataxis");
await writeSkillTree(path.join(rootDir, "other-skill"), "Other Skill");
const result = await svc.importFromSource(companyId, `npx skills add ${rootDir} --skill diataxis`);
const imported = result.imported[0]!;
expect(result.imported).toHaveLength(1);
expect(imported.slug).toBe("diataxis");
expect(imported.fileInventory).toEqual([
{ path: "assets/logo.svg", kind: "asset" },
{ path: "references/how-to-guide-template.md", kind: "reference" },
{ path: "scripts/bootstrap.sh", kind: "script" },
{ path: "SKILL.md", kind: "skill" },
]);
});
it("rejects skill inventory refresh for a missing company", async () => {
await expect(svc.list(randomUUID())).rejects.toMatchObject({
status: 404,
+9 -2
View File
@@ -1118,10 +1118,17 @@ async function readLocalSkillImports(companyId: string, sourcePath: string): Pro
const imports: ImportedSkill[] = [];
for (const skillPath of skillPaths) {
const skillDir = path.posix.dirname(skillPath);
const skillInventoryRoot = skillDir === "." ? "" : skillDir;
const inventory = allFiles
.filter((entry) => entry === skillPath || entry.startsWith(`${skillDir}/`))
.filter((entry) =>
entry === skillPath
|| (skillInventoryRoot ? entry.startsWith(`${skillInventoryRoot}/`) : true))
.map((entry) => {
const relative = entry === skillPath ? "SKILL.md" : entry.slice(skillDir.length + 1);
const relative = entry === skillPath
? "SKILL.md"
: skillInventoryRoot
? entry.slice(skillInventoryRoot.length + 1)
: entry;
return {
path: normalizePortablePath(relative),
kind: classifyInventoryKind(relative),