Set sourceType to skills_sh for skills imported from skills.sh URLs
When skills are imported via skills.sh URLs or key-style imports (org/repo/skill), the stored sourceType is now "skills_sh" with the original skills.sh URL as sourceLocator, instead of "github" with the resolved GitHub URL. - Add "skills_sh" to CompanySkillSourceType and CompanySkillSourceBadge - Track originalSkillsShUrl in parseSkillImportSourceInput - Override sourceType/sourceLocator in importFromSource for skills.sh - Handle skills_sh in key derivation, source info, update checks, file reads, portability export, and UI badge rendering Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -35,32 +35,36 @@ describe("company skill import source parsing", () => {
|
||||
|
||||
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
||||
expect(parsed.requestedSkillSlug).toBe("find-skills");
|
||||
expect(parsed.originalSkillsShUrl).toBeNull();
|
||||
expect(parsed.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("parses owner/repo/skill shorthand as a GitHub repo plus requested skill", () => {
|
||||
it("parses owner/repo/skill shorthand as skills.sh-managed", () => {
|
||||
const parsed = parseSkillImportSourceInput("vercel-labs/skills/find-skills");
|
||||
|
||||
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
||||
expect(parsed.requestedSkillSlug).toBe("find-skills");
|
||||
expect(parsed.originalSkillsShUrl).toBe("https://skills.sh/vercel-labs/skills/find-skills");
|
||||
});
|
||||
|
||||
it("resolves skills.sh URL with org/repo/skill to GitHub repo", () => {
|
||||
it("resolves skills.sh URL with org/repo/skill to GitHub repo and preserves original URL", () => {
|
||||
const parsed = parseSkillImportSourceInput(
|
||||
"https://skills.sh/google-labs-code/stitch-skills/design-md",
|
||||
);
|
||||
|
||||
expect(parsed.resolvedSource).toBe("https://github.com/google-labs-code/stitch-skills");
|
||||
expect(parsed.requestedSkillSlug).toBe("design-md");
|
||||
expect(parsed.originalSkillsShUrl).toBe("https://skills.sh/google-labs-code/stitch-skills/design-md");
|
||||
});
|
||||
|
||||
it("resolves skills.sh URL with org/repo (no skill) to GitHub repo", () => {
|
||||
it("resolves skills.sh URL with org/repo (no skill) to GitHub repo and preserves original URL", () => {
|
||||
const parsed = parseSkillImportSourceInput(
|
||||
"https://skills.sh/vercel-labs/skills",
|
||||
);
|
||||
|
||||
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
||||
expect(parsed.requestedSkillSlug).toBeNull();
|
||||
expect(parsed.originalSkillsShUrl).toBe("https://skills.sh/vercel-labs/skills");
|
||||
});
|
||||
|
||||
it("parses skills.sh commands whose requested skill differs from the folder name", () => {
|
||||
@@ -70,6 +74,14 @@ describe("company skill import source parsing", () => {
|
||||
|
||||
expect(parsed.resolvedSource).toBe("https://github.com/remotion-dev/skills");
|
||||
expect(parsed.requestedSkillSlug).toBe("remotion-best-practices");
|
||||
expect(parsed.originalSkillsShUrl).toBeNull();
|
||||
});
|
||||
|
||||
it("does not set originalSkillsShUrl for owner/repo shorthand", () => {
|
||||
const parsed = parseSkillImportSourceInput("vercel-labs/skills");
|
||||
|
||||
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
||||
expect(parsed.originalSkillsShUrl).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ function mermaidEscape(s: string): string {
|
||||
function skillSourceLabel(skill: CompanyPortabilityManifest["skills"][number]): string {
|
||||
if (skill.sourceLocator) {
|
||||
// For GitHub or URL sources, render as a markdown link
|
||||
if (skill.sourceType === "github" || skill.sourceType === "url") {
|
||||
if (skill.sourceType === "github" || skill.sourceType === "skills_sh" || skill.sourceType === "url") {
|
||||
return `[${skill.sourceType}](${skill.sourceLocator})`;
|
||||
}
|
||||
return skill.sourceLocator;
|
||||
|
||||
@@ -119,7 +119,7 @@ function deriveManifestSkillKey(
|
||||
const sourceKind = asString(metadata?.sourceKind);
|
||||
const owner = normalizeSkillSlug(asString(metadata?.owner));
|
||||
const repo = normalizeSkillSlug(asString(metadata?.repo));
|
||||
if ((sourceType === "github" || sourceKind === "github") && owner && repo) {
|
||||
if ((sourceType === "github" || sourceType === "skills_sh" || sourceKind === "github" || sourceKind === "skills_sh") && owner && repo) {
|
||||
return `${owner}/${repo}/${slug}`;
|
||||
}
|
||||
if (sourceKind === "paperclip_bundled") {
|
||||
@@ -246,10 +246,10 @@ function deriveSkillExportDirCandidates(
|
||||
pushSuffix("paperclip");
|
||||
}
|
||||
|
||||
if (skill.sourceType === "github") {
|
||||
if (skill.sourceType === "github" || skill.sourceType === "skills_sh") {
|
||||
pushSuffix(asString(metadata?.repo));
|
||||
pushSuffix(asString(metadata?.owner));
|
||||
pushSuffix("github");
|
||||
pushSuffix(skill.sourceType === "skills_sh" ? "skills_sh" : "github");
|
||||
} else if (skill.sourceType === "url") {
|
||||
try {
|
||||
pushSuffix(skill.sourceLocator ? new URL(skill.sourceLocator).host : null);
|
||||
@@ -1178,7 +1178,7 @@ async function buildSkillSourceEntry(skill: CompanySkill) {
|
||||
};
|
||||
}
|
||||
|
||||
if (skill.sourceType === "github") {
|
||||
if (skill.sourceType === "github" || skill.sourceType === "skills_sh") {
|
||||
const owner = asString(metadata?.owner);
|
||||
const repo = asString(metadata?.repo);
|
||||
const repoSkillDir = asString(metadata?.repoSkillDir);
|
||||
@@ -1207,7 +1207,7 @@ function shouldReferenceSkillOnExport(skill: CompanySkill, expandReferencedSkill
|
||||
if (expandReferencedSkills) return false;
|
||||
const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null;
|
||||
if (asString(metadata?.sourceKind) === "paperclip_bundled") return true;
|
||||
return skill.sourceType === "github" || skill.sourceType === "url";
|
||||
return skill.sourceType === "github" || skill.sourceType === "skills_sh" || skill.sourceType === "url";
|
||||
}
|
||||
|
||||
async function buildReferencedSkillMarkdown(skill: CompanySkill) {
|
||||
|
||||
@@ -66,6 +66,7 @@ export type ImportPackageSkillResult = {
|
||||
type ParsedSkillImportSource = {
|
||||
resolvedSource: string;
|
||||
requestedSkillSlug: string | null;
|
||||
originalSkillsShUrl: string | null;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
@@ -251,7 +252,7 @@ function deriveCanonicalSkillKey(
|
||||
|
||||
const owner = normalizeSkillSlug(asString(metadata?.owner));
|
||||
const repo = normalizeSkillSlug(asString(metadata?.repo));
|
||||
if ((input.sourceType === "github" || sourceKind === "github") && owner && repo) {
|
||||
if ((input.sourceType === "github" || input.sourceType === "skills_sh" || sourceKind === "github" || sourceKind === "skills_sh") && owner && repo) {
|
||||
return `${owner}/${repo}/${slug}`;
|
||||
}
|
||||
|
||||
@@ -561,11 +562,13 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport
|
||||
throw unprocessable("Skill source is required.");
|
||||
}
|
||||
|
||||
// Key-style imports (org/repo/skill) originate from the skills.sh registry
|
||||
if (!/^https?:\/\//i.test(normalizedSource) && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalizedSource)) {
|
||||
const [owner, repo, skillSlugRaw] = normalizedSource.split("/");
|
||||
return {
|
||||
resolvedSource: `https://github.com/${owner}/${repo}`,
|
||||
requestedSkillSlug: normalizeSkillSlug(skillSlugRaw),
|
||||
originalSkillsShUrl: `https://skills.sh/${owner}/${repo}/${skillSlugRaw}`,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
@@ -574,6 +577,7 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport
|
||||
return {
|
||||
resolvedSource: `https://github.com/${normalizedSource}`,
|
||||
requestedSkillSlug,
|
||||
originalSkillsShUrl: null,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
@@ -585,6 +589,7 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport
|
||||
return {
|
||||
resolvedSource: `https://github.com/${owner}/${repo}`,
|
||||
requestedSkillSlug: skillSlugRaw ? normalizeSkillSlug(skillSlugRaw) : requestedSkillSlug,
|
||||
originalSkillsShUrl: normalizedSource,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
@@ -592,6 +597,7 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport
|
||||
return {
|
||||
resolvedSource: normalizedSource,
|
||||
requestedSkillSlug,
|
||||
originalSkillsShUrl: null,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
@@ -1292,6 +1298,18 @@ function deriveSkillSourceInfo(skill: CompanySkill): {
|
||||
};
|
||||
}
|
||||
|
||||
if (skill.sourceType === "skills_sh") {
|
||||
const owner = asString(metadata.owner) ?? null;
|
||||
const repo = asString(metadata.repo) ?? null;
|
||||
return {
|
||||
editable: false,
|
||||
editableReason: "Skills.sh-managed skills are read-only.",
|
||||
sourceLabel: skill.sourceLocator ?? (owner && repo ? `${owner}/${repo}` : null),
|
||||
sourceBadge: "skills_sh",
|
||||
sourcePath: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (skill.sourceType === "github") {
|
||||
const owner = asString(metadata.owner) ?? null;
|
||||
const repo = asString(metadata.repo) ?? null;
|
||||
@@ -1543,7 +1561,7 @@ export function companySkillService(db: Db) {
|
||||
const skill = await getById(skillId);
|
||||
if (!skill || skill.companyId !== companyId) return null;
|
||||
|
||||
if (skill.sourceType !== "github") {
|
||||
if (skill.sourceType !== "github" && skill.sourceType !== "skills_sh") {
|
||||
return {
|
||||
supported: false,
|
||||
reason: "Only GitHub-managed skills support update checks.",
|
||||
@@ -1603,7 +1621,7 @@ export function companySkillService(db: Db) {
|
||||
} else {
|
||||
throw notFound("Skill file not found");
|
||||
}
|
||||
} else if (skill.sourceType === "github") {
|
||||
} else if (skill.sourceType === "github" || skill.sourceType === "skills_sh") {
|
||||
const metadata = getSkillMeta(skill);
|
||||
const owner = asString(metadata.owner);
|
||||
const repo = asString(metadata.repo);
|
||||
@@ -2202,6 +2220,17 @@ export function companySkillService(db: Db) {
|
||||
: "No skills were found in the provided source.",
|
||||
);
|
||||
}
|
||||
// Override sourceType/sourceLocator for skills imported via skills.sh
|
||||
if (parsed.originalSkillsShUrl) {
|
||||
for (const skill of filteredSkills) {
|
||||
skill.sourceType = "skills_sh";
|
||||
skill.sourceLocator = parsed.originalSkillsShUrl;
|
||||
if (skill.metadata) {
|
||||
(skill.metadata as Record<string, unknown>).sourceKind = "skills_sh";
|
||||
}
|
||||
skill.key = deriveCanonicalSkillKey(companyId, skill);
|
||||
}
|
||||
}
|
||||
const imported = await upsertImportedSkills(companyId, filteredSkills);
|
||||
return { imported, warnings };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user