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:
@@ -1,10 +1,10 @@
|
|||||||
export type CompanySkillSourceType = "local_path" | "github" | "url" | "catalog";
|
export type CompanySkillSourceType = "local_path" | "github" | "url" | "catalog" | "skills_sh";
|
||||||
|
|
||||||
export type CompanySkillTrustLevel = "markdown_only" | "assets" | "scripts_executables";
|
export type CompanySkillTrustLevel = "markdown_only" | "assets" | "scripts_executables";
|
||||||
|
|
||||||
export type CompanySkillCompatibility = "compatible" | "unknown" | "invalid";
|
export type CompanySkillCompatibility = "compatible" | "unknown" | "invalid";
|
||||||
|
|
||||||
export type CompanySkillSourceBadge = "paperclip" | "github" | "local" | "url" | "catalog";
|
export type CompanySkillSourceBadge = "paperclip" | "github" | "local" | "url" | "catalog" | "skills_sh";
|
||||||
|
|
||||||
export interface CompanySkillFileInventoryEntry {
|
export interface CompanySkillFileInventoryEntry {
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const companySkillSourceTypeSchema = z.enum(["local_path", "github", "url", "catalog"]);
|
export const companySkillSourceTypeSchema = z.enum(["local_path", "github", "url", "catalog", "skills_sh"]);
|
||||||
export const companySkillTrustLevelSchema = z.enum(["markdown_only", "assets", "scripts_executables"]);
|
export const companySkillTrustLevelSchema = z.enum(["markdown_only", "assets", "scripts_executables"]);
|
||||||
export const companySkillCompatibilitySchema = z.enum(["compatible", "unknown", "invalid"]);
|
export const companySkillCompatibilitySchema = z.enum(["compatible", "unknown", "invalid"]);
|
||||||
export const companySkillSourceBadgeSchema = z.enum(["paperclip", "github", "local", "url", "catalog"]);
|
export const companySkillSourceBadgeSchema = z.enum(["paperclip", "github", "local", "url", "catalog", "skills_sh"]);
|
||||||
|
|
||||||
export const companySkillFileInventoryEntrySchema = z.object({
|
export const companySkillFileInventoryEntrySchema = z.object({
|
||||||
path: z.string().min(1),
|
path: z.string().min(1),
|
||||||
|
|||||||
@@ -35,32 +35,36 @@ describe("company skill import source parsing", () => {
|
|||||||
|
|
||||||
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
||||||
expect(parsed.requestedSkillSlug).toBe("find-skills");
|
expect(parsed.requestedSkillSlug).toBe("find-skills");
|
||||||
|
expect(parsed.originalSkillsShUrl).toBeNull();
|
||||||
expect(parsed.warnings).toEqual([]);
|
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");
|
const parsed = parseSkillImportSourceInput("vercel-labs/skills/find-skills");
|
||||||
|
|
||||||
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
||||||
expect(parsed.requestedSkillSlug).toBe("find-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(
|
const parsed = parseSkillImportSourceInput(
|
||||||
"https://skills.sh/google-labs-code/stitch-skills/design-md",
|
"https://skills.sh/google-labs-code/stitch-skills/design-md",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(parsed.resolvedSource).toBe("https://github.com/google-labs-code/stitch-skills");
|
expect(parsed.resolvedSource).toBe("https://github.com/google-labs-code/stitch-skills");
|
||||||
expect(parsed.requestedSkillSlug).toBe("design-md");
|
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(
|
const parsed = parseSkillImportSourceInput(
|
||||||
"https://skills.sh/vercel-labs/skills",
|
"https://skills.sh/vercel-labs/skills",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills");
|
||||||
expect(parsed.requestedSkillSlug).toBeNull();
|
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", () => {
|
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.resolvedSource).toBe("https://github.com/remotion-dev/skills");
|
||||||
expect(parsed.requestedSkillSlug).toBe("remotion-best-practices");
|
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 {
|
function skillSourceLabel(skill: CompanyPortabilityManifest["skills"][number]): string {
|
||||||
if (skill.sourceLocator) {
|
if (skill.sourceLocator) {
|
||||||
// For GitHub or URL sources, render as a markdown link
|
// 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.sourceType}](${skill.sourceLocator})`;
|
||||||
}
|
}
|
||||||
return skill.sourceLocator;
|
return skill.sourceLocator;
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ function deriveManifestSkillKey(
|
|||||||
const sourceKind = asString(metadata?.sourceKind);
|
const sourceKind = asString(metadata?.sourceKind);
|
||||||
const owner = normalizeSkillSlug(asString(metadata?.owner));
|
const owner = normalizeSkillSlug(asString(metadata?.owner));
|
||||||
const repo = normalizeSkillSlug(asString(metadata?.repo));
|
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}`;
|
return `${owner}/${repo}/${slug}`;
|
||||||
}
|
}
|
||||||
if (sourceKind === "paperclip_bundled") {
|
if (sourceKind === "paperclip_bundled") {
|
||||||
@@ -246,10 +246,10 @@ function deriveSkillExportDirCandidates(
|
|||||||
pushSuffix("paperclip");
|
pushSuffix("paperclip");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skill.sourceType === "github") {
|
if (skill.sourceType === "github" || skill.sourceType === "skills_sh") {
|
||||||
pushSuffix(asString(metadata?.repo));
|
pushSuffix(asString(metadata?.repo));
|
||||||
pushSuffix(asString(metadata?.owner));
|
pushSuffix(asString(metadata?.owner));
|
||||||
pushSuffix("github");
|
pushSuffix(skill.sourceType === "skills_sh" ? "skills_sh" : "github");
|
||||||
} else if (skill.sourceType === "url") {
|
} else if (skill.sourceType === "url") {
|
||||||
try {
|
try {
|
||||||
pushSuffix(skill.sourceLocator ? new URL(skill.sourceLocator).host : null);
|
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 owner = asString(metadata?.owner);
|
||||||
const repo = asString(metadata?.repo);
|
const repo = asString(metadata?.repo);
|
||||||
const repoSkillDir = asString(metadata?.repoSkillDir);
|
const repoSkillDir = asString(metadata?.repoSkillDir);
|
||||||
@@ -1207,7 +1207,7 @@ function shouldReferenceSkillOnExport(skill: CompanySkill, expandReferencedSkill
|
|||||||
if (expandReferencedSkills) return false;
|
if (expandReferencedSkills) return false;
|
||||||
const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null;
|
const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null;
|
||||||
if (asString(metadata?.sourceKind) === "paperclip_bundled") return true;
|
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) {
|
async function buildReferencedSkillMarkdown(skill: CompanySkill) {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export type ImportPackageSkillResult = {
|
|||||||
type ParsedSkillImportSource = {
|
type ParsedSkillImportSource = {
|
||||||
resolvedSource: string;
|
resolvedSource: string;
|
||||||
requestedSkillSlug: string | null;
|
requestedSkillSlug: string | null;
|
||||||
|
originalSkillsShUrl: string | null;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -251,7 +252,7 @@ function deriveCanonicalSkillKey(
|
|||||||
|
|
||||||
const owner = normalizeSkillSlug(asString(metadata?.owner));
|
const owner = normalizeSkillSlug(asString(metadata?.owner));
|
||||||
const repo = normalizeSkillSlug(asString(metadata?.repo));
|
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}`;
|
return `${owner}/${repo}/${slug}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,11 +562,13 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport
|
|||||||
throw unprocessable("Skill source is required.");
|
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)) {
|
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("/");
|
const [owner, repo, skillSlugRaw] = normalizedSource.split("/");
|
||||||
return {
|
return {
|
||||||
resolvedSource: `https://github.com/${owner}/${repo}`,
|
resolvedSource: `https://github.com/${owner}/${repo}`,
|
||||||
requestedSkillSlug: normalizeSkillSlug(skillSlugRaw),
|
requestedSkillSlug: normalizeSkillSlug(skillSlugRaw),
|
||||||
|
originalSkillsShUrl: `https://skills.sh/${owner}/${repo}/${skillSlugRaw}`,
|
||||||
warnings,
|
warnings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -574,6 +577,7 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport
|
|||||||
return {
|
return {
|
||||||
resolvedSource: `https://github.com/${normalizedSource}`,
|
resolvedSource: `https://github.com/${normalizedSource}`,
|
||||||
requestedSkillSlug,
|
requestedSkillSlug,
|
||||||
|
originalSkillsShUrl: null,
|
||||||
warnings,
|
warnings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -585,6 +589,7 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport
|
|||||||
return {
|
return {
|
||||||
resolvedSource: `https://github.com/${owner}/${repo}`,
|
resolvedSource: `https://github.com/${owner}/${repo}`,
|
||||||
requestedSkillSlug: skillSlugRaw ? normalizeSkillSlug(skillSlugRaw) : requestedSkillSlug,
|
requestedSkillSlug: skillSlugRaw ? normalizeSkillSlug(skillSlugRaw) : requestedSkillSlug,
|
||||||
|
originalSkillsShUrl: normalizedSource,
|
||||||
warnings,
|
warnings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -592,6 +597,7 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport
|
|||||||
return {
|
return {
|
||||||
resolvedSource: normalizedSource,
|
resolvedSource: normalizedSource,
|
||||||
requestedSkillSlug,
|
requestedSkillSlug,
|
||||||
|
originalSkillsShUrl: null,
|
||||||
warnings,
|
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") {
|
if (skill.sourceType === "github") {
|
||||||
const owner = asString(metadata.owner) ?? null;
|
const owner = asString(metadata.owner) ?? null;
|
||||||
const repo = asString(metadata.repo) ?? null;
|
const repo = asString(metadata.repo) ?? null;
|
||||||
@@ -1543,7 +1561,7 @@ export function companySkillService(db: Db) {
|
|||||||
const skill = await getById(skillId);
|
const skill = await getById(skillId);
|
||||||
if (!skill || skill.companyId !== companyId) return null;
|
if (!skill || skill.companyId !== companyId) return null;
|
||||||
|
|
||||||
if (skill.sourceType !== "github") {
|
if (skill.sourceType !== "github" && skill.sourceType !== "skills_sh") {
|
||||||
return {
|
return {
|
||||||
supported: false,
|
supported: false,
|
||||||
reason: "Only GitHub-managed skills support update checks.",
|
reason: "Only GitHub-managed skills support update checks.",
|
||||||
@@ -1603,7 +1621,7 @@ export function companySkillService(db: Db) {
|
|||||||
} else {
|
} else {
|
||||||
throw notFound("Skill file not found");
|
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 metadata = getSkillMeta(skill);
|
||||||
const owner = asString(metadata.owner);
|
const owner = asString(metadata.owner);
|
||||||
const repo = asString(metadata.repo);
|
const repo = asString(metadata.repo);
|
||||||
@@ -2202,6 +2220,17 @@ export function companySkillService(db: Db) {
|
|||||||
: "No skills were found in the provided source.",
|
: "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);
|
const imported = await upsertImportedSkills(companyId, filteredSkills);
|
||||||
return { imported, warnings };
|
return { imported, warnings };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ function sourceMeta(sourceBadge: CompanySkillSourceBadge, sourceLabel: string |
|
|||||||
normalizedLabel.includes("skills.sh") || normalizedLabel.includes("vercel-labs/skills");
|
normalizedLabel.includes("skills.sh") || normalizedLabel.includes("vercel-labs/skills");
|
||||||
|
|
||||||
switch (sourceBadge) {
|
switch (sourceBadge) {
|
||||||
|
case "skills_sh":
|
||||||
|
return { icon: VercelMark, label: sourceLabel ?? "skills.sh", managedLabel: "skills.sh managed" };
|
||||||
case "github":
|
case "github":
|
||||||
return isSkillsShManaged
|
return isSkillsShManaged
|
||||||
? { icon: VercelMark, label: sourceLabel ?? "skills.sh", managedLabel: "skills.sh managed" }
|
? { icon: VercelMark, label: sourceLabel ?? "skills.sh", managedLabel: "skills.sh managed" }
|
||||||
|
|||||||
Reference in New Issue
Block a user