Skip to content

Commit e074a2d

Browse files
committed
fix(desktop): finalize skill variant source metadata
1 parent f43023e commit e074a2d

46 files changed

Lines changed: 2087 additions & 86 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/src/main/ipc/skill/local-repo-handlers.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { ipcMain } from "electron";
22
import { IPC_CHANNELS } from "@prompthub/shared/constants";
33
import { SkillInstaller } from "../../services/skill-installer";
4-
import { buildSkillSyncUpdateFromRepo } from "../../services/skill-repo-sync";
4+
import {
5+
buildSkillSyncUpdateFromRepo,
6+
computeRepoDirectoryFingerprint,
7+
} from "../../services/skill-repo-sync";
58
import type { SkillIPCContext } from "./shared";
69
import { ensureLocalRepoPath, readCurrentFilesSnapshot } from "./shared";
710

@@ -82,7 +85,14 @@ async function syncSkillFromRepo(
8285
return skill;
8386
}
8487

85-
const nextUpdate = buildSkillSyncUpdateFromRepo(skill, skillMdFile.content);
88+
const directoryFingerprint = await computeRepoDirectoryFingerprint(
89+
resolvedRepoPath,
90+
);
91+
const nextUpdate = buildSkillSyncUpdateFromRepo(
92+
skill,
93+
skillMdFile.content,
94+
directoryFingerprint,
95+
);
8696
if (!nextUpdate) {
8797
return skill;
8898
}
@@ -301,9 +311,11 @@ export function registerSkillLocalRepoHandlers({ db }: SkillIPCContext): void {
301311
content,
302312
);
303313
if (relativePath.toLowerCase() === "skill.md") {
314+
const nextFingerprint = await computeRepoDirectoryFingerprint(repoPath);
304315
db.update(skillId, {
305316
content,
306317
instructions: content,
318+
directory_fingerprint: nextFingerprint,
307319
});
308320
}
309321
return result;

apps/desktop/src/main/services/skill-import-sanitize.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export interface ImportedSkillDraft {
2727
fallbackTags?: unknown;
2828
instructions?: unknown;
2929
source_url?: unknown;
30+
source_label?: unknown;
31+
source_branch?: unknown;
32+
source_directory?: unknown;
33+
canonical_skill_path?: unknown;
3034
local_repo_path?: unknown;
3135
icon_url?: unknown;
3236
icon_emoji?: unknown;
@@ -45,6 +49,10 @@ export interface SanitizedImportedSkill {
4549
tags: string[];
4650
instructions?: string;
4751
source_url?: string;
52+
source_label?: string;
53+
source_branch?: string;
54+
source_directory?: string;
55+
canonical_skill_path?: string;
4856
local_repo_path?: string;
4957
icon_url?: string;
5058
icon_emoji?: string;
@@ -135,6 +143,18 @@ export function sanitizeImportedSkillDraft(
135143
tags: sanitizeImportedTags(draft.tags, draft.fallbackTags, defaultTags),
136144
instructions: sanitizeImportedString(draft.instructions),
137145
source_url: sanitizeImportedString(draft.source_url, undefined, 500000),
146+
source_label: sanitizeImportedString(draft.source_label, undefined, 500000),
147+
source_branch: sanitizeImportedString(draft.source_branch, undefined, 256),
148+
source_directory: sanitizeImportedString(
149+
draft.source_directory,
150+
undefined,
151+
500000,
152+
),
153+
canonical_skill_path: sanitizeImportedString(
154+
draft.canonical_skill_path,
155+
undefined,
156+
500000,
157+
),
138158
local_repo_path: sanitizeImportedString(
139159
draft.local_repo_path,
140160
undefined,

apps/desktop/src/main/services/skill-installer-export.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export function exportAsJson(skill: {
7373
instructions?: string;
7474
protocol_type?: string;
7575
source_url?: string;
76+
source_label?: string;
77+
source_branch?: string;
78+
source_directory?: string;
79+
canonical_skill_path?: string;
7680
icon_url?: string;
7781
icon_emoji?: string;
7882
icon_background?: string;
@@ -86,6 +90,10 @@ export function exportAsJson(skill: {
8690
instructions: skill.instructions || "",
8791
protocol_type: skill.protocol_type || "skill",
8892
source_url: skill.source_url || "",
93+
source_label: skill.source_label || "",
94+
source_branch: skill.source_branch || "",
95+
source_directory: skill.source_directory || "",
96+
canonical_skill_path: skill.canonical_skill_path || "",
8997
icon_url: skill.icon_url || "",
9098
icon_emoji: skill.icon_emoji || "",
9199
icon_background: skill.icon_background || "",
@@ -121,6 +129,10 @@ export async function importFromJson(
121129
compatibility: data.compatibility,
122130
protocol_type: data.protocol_type,
123131
source_url: data.source_url,
132+
source_label: data.source_label,
133+
source_branch: data.source_branch,
134+
source_directory: data.source_directory,
135+
canonical_skill_path: data.canonical_skill_path,
124136
},
125137
{ defaultTags: ["imported"] },
126138
);
@@ -146,6 +158,10 @@ export async function importFromJson(
146158
prerequisites: sanitized.prerequisites,
147159
compatibility: sanitized.compatibility,
148160
source_url: sanitized.source_url,
161+
source_label: sanitized.source_label,
162+
source_branch: sanitized.source_branch,
163+
source_directory: sanitized.source_directory,
164+
canonical_skill_path: sanitized.canonical_skill_path,
149165
});
150166

151167
return skill.id;

apps/desktop/src/main/services/skill-installer-repo.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as fs from "fs/promises";
88
import * as path from "path";
99
import type {
1010
Skill,
11+
SkillLocalFileBufferEntry,
1112
SkillLocalFileEntry,
1213
SkillLocalFileTreeEntry,
1314
} from "@prompthub/shared/types";
@@ -25,11 +26,6 @@ import {
2526
validateSkillName,
2627
} from "./skill-installer-internal";
2728

28-
export interface SkillLocalFileBufferEntry {
29-
path: string;
30-
data: Uint8Array;
31-
}
32-
3329
export interface CopyRepoByPathToDirectoryOptions {
3430
ifExists?: "overwrite" | "skip" | "error";
3531
mode?: "copy" | "symlink";

apps/desktop/src/main/services/skill-installer.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ export class SkillInstaller {
347347

348348
// Create Skill in DB first, then move the cloned repo into the managed
349349
// variant container so all My Skills entries share one disk layout.
350+
const repoFiles = await this.readLocalRepoFileBuffersByPath(skillDir);
351+
const sourceDirectory = path.relative(installDir, skillDir).replace(/\\/g, "/") || undefined;
350352
const skill = db.create({
351353
name: manifest.name || repoName,
352354
description: manifest.description || `Installed from ${url}`,
@@ -356,7 +358,11 @@ export class SkillInstaller {
356358
instructions: manifest.instructions || "",
357359
protocol_type: "skill",
358360
source_url: url,
361+
source_label: `${userDir}/${repoName}`,
362+
source_directory: sourceDirectory,
363+
canonical_skill_path: sourceDirectory ? `${sourceDirectory}/SKILL.md` : "SKILL.md",
359364
local_repo_path: installDir,
365+
directory_fingerprint: computeDirectoryFingerprint(repoFiles),
360366
is_favorite: false,
361367
tags: [],
362368
original_tags: manifest.tags || ["github"],
@@ -474,9 +480,9 @@ export class SkillInstaller {
474480
);
475481
}
476482

477-
const sanitized = sanitizeImportedSkillDraft(
478-
{
479-
name: skillName,
483+
const sanitized = sanitizeImportedSkillDraft(
484+
{
485+
name: skillName,
480486
description: parsed?.frontmatter.description,
481487
fallbackDescription:
482488
manifest.description ||
@@ -494,6 +500,7 @@ export class SkillInstaller {
494500
},
495501
{ defaultTags: [] },
496502
);
503+
const canonicalSkillPath = options?.repoSourceDir ? "SKILL.md" : undefined;
497504

498505
// Save files first, then create DB record to avoid orphaned records
499506
const createdSkill = db.create({
@@ -508,7 +515,9 @@ export class SkillInstaller {
508515
original_tags: sanitized.tags,
509516
is_favorite: false,
510517
source_url: sanitized.source_url,
518+
source_label: options?.sourceUrl,
511519
local_repo_path: sanitized.local_repo_path,
520+
canonical_skill_path: canonicalSkillPath,
512521
});
513522

514523
let localRepoPath: string | undefined;
@@ -526,7 +535,16 @@ export class SkillInstaller {
526535
}
527536

528537
if (localRepoPath && createdSkill.local_repo_path !== localRepoPath) {
529-
db.update(createdSkill.id, { local_repo_path: localRepoPath });
538+
const repoFiles = await this.readLocalRepoFileBuffersByPath(localRepoPath);
539+
db.update(createdSkill.id, {
540+
local_repo_path: localRepoPath,
541+
directory_fingerprint: computeDirectoryFingerprint(repoFiles),
542+
});
543+
} else if (localRepoPath) {
544+
const repoFiles = await this.readLocalRepoFileBuffersByPath(localRepoPath);
545+
db.update(createdSkill.id, {
546+
directory_fingerprint: computeDirectoryFingerprint(repoFiles),
547+
});
530548
}
531549

532550
return createdSkill.id;
@@ -659,9 +677,7 @@ export class SkillInstaller {
659677
source_branch: normalizedBranch,
660678
source_directory: normalizedDirectory || undefined,
661679
canonical_skill_path: skill.filePath,
662-
directory_fingerprint: computeDirectoryFingerprint([
663-
{ path: "SKILL.md", content: skill.instructions, isDirectory: false },
664-
]),
680+
directory_fingerprint: skill.directory_fingerprint,
665681
description: builtin?.description || skill.description || `${skill.name} skill`,
666682
category: builtin?.category || "general",
667683
icon_url: builtin?.icon_url,
@@ -873,6 +889,9 @@ export class SkillInstaller {
873889
);
874890

875891
skillMap.set(skillFolderPath, {
892+
directory_fingerprint: computeDirectoryFingerprint(
893+
await this.readLocalRepoFileBuffersByPath(skillFolderPath),
894+
),
876895
name: sanitized.name!,
877896
description: sanitized.description || manifest.description,
878897
version: sanitized.version,

apps/desktop/src/main/services/skill-repo-sync.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Skill, UpdateSkillParams } from "@prompthub/shared/types";
2+
import { computeDirectoryFingerprint } from "@prompthub/shared/utils/skill-identity";
23
import { sanitizeImportedSkillDraft } from "./skill-import-sanitize";
34
import { parseSkillMd } from "./skill-validator";
45
import { SkillInstaller } from "./skill-installer";
@@ -29,6 +30,7 @@ function normalizeCompatibility(compatibility?: string): string[] | undefined {
2930
export function buildSkillSyncUpdateFromRepo(
3031
skill: Skill,
3132
skillMdContent: string,
33+
directoryFingerprint?: string,
3234
): UpdateSkillParams | null {
3335
const parsed = parseSkillMd(skillMdContent);
3436
const sanitized = sanitizeImportedSkillDraft(
@@ -91,9 +93,21 @@ export function buildSkillSyncUpdateFromRepo(
9193
changed = true;
9294
}
9395

96+
if (directoryFingerprint !== undefined && directoryFingerprint !== skill.directory_fingerprint) {
97+
update.directory_fingerprint = directoryFingerprint;
98+
changed = true;
99+
}
100+
94101
return changed ? update : null;
95102
}
96103

104+
export async function computeRepoDirectoryFingerprint(
105+
repoPath: string,
106+
): Promise<string> {
107+
const entries = await SkillInstaller.readLocalRepoFileBuffersByPath(repoPath);
108+
return computeDirectoryFingerprint(entries);
109+
}
110+
97111
/**
98112
* Check whether metadata-only fields (description, author, name, tags) changed.
99113
* Returns true when the SKILL.md frontmatter should be rewritten to stay in sync

apps/desktop/src/renderer/components/skill/SkillGalleryCard.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { useTranslation } from "react-i18next";
1111
import type { Skill } from "@prompthub/shared/types";
1212
import { SkillIcon } from "./SkillIcon";
1313
import { getRuntimeCapabilities } from "../../runtime";
14+
import { SkillVariantBadgeList } from "./SkillVariantBadgeList";
15+
import { buildSkillVariantBadges } from "../../services/skill-variant-badges";
1416

1517
function normalizeStringArray(value: unknown): string[] {
1618
if (Array.isArray(value)) {
@@ -59,6 +61,7 @@ function SkillGalleryCardComponent({
5961
const { t } = useTranslation();
6062
const runtimeCapabilities = getRuntimeCapabilities();
6163
const visibleTags = normalizeStringArray(skill.tags).slice(0, 4);
64+
const variantBadges = buildSkillVariantBadges(skill, t);
6265

6366
return (
6467
<div
@@ -181,6 +184,7 @@ function SkillGalleryCardComponent({
181184
{skill.description ||
182185
t("skill.defaultDescription", "技能描述,帮助 AI 理解何时使用此技能")}
183186
</p>
187+
<SkillVariantBadgeList badges={variantBadges} className="mb-3 flex flex-wrap gap-1.5" />
184188
{visibleTags.length > 0 ? (
185189
<div className="flex flex-wrap gap-1.5">
186190
{visibleTags.map((tag) => (

apps/desktop/src/renderer/components/skill/SkillListView.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { filterDetectedPlatforms } from "../../services/platform-visibility";
2121
import type { Skill, SkillSafetyLevel } from "@prompthub/shared/types";
2222
import type { SkillPlatform } from "@prompthub/shared/constants/platforms";
2323
import { getRuntimeCapabilities } from "../../runtime";
24+
import { SkillVariantBadgeList } from "./SkillVariantBadgeList";
25+
import { buildSkillVariantBadges } from "../../services/skill-variant-badges";
2426

2527
function normalizeStringArray(value: unknown): string[] {
2628
if (Array.isArray(value)) {
@@ -438,19 +440,23 @@ export function SkillListView({
438440
<p className="text-xs text-muted-foreground truncate mt-0.5">
439441
{skill.description || t("skill.defaultDescription")}
440442
</p>
441-
{visibleTags.length > 0 ? (
442-
<div className="mt-2 flex flex-wrap gap-1.5">
443-
{visibleTags.map((tag) => (
443+
{visibleTags.length > 0 ? (
444+
<div className="mt-2 flex flex-wrap gap-1.5">
445+
{visibleTags.map((tag) => (
444446
<span
445447
key={tag}
446448
className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary"
447449
>
448450
{tag}
449451
</span>
450-
))}
451-
</div>
452-
) : null}
453-
</div>
452+
))}
453+
</div>
454+
) : null}
455+
<SkillVariantBadgeList
456+
badges={buildSkillVariantBadges(skill, t)}
457+
className="mt-2 flex flex-wrap gap-1.5"
458+
/>
459+
</div>
454460

455461
{/* Platform indicators */}
456462
{runtimeCapabilities.skillPlatformIntegration && totalPlatforms > 0 && (

apps/desktop/src/renderer/components/skill/SkillStoreCard.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { CheckIcon, DownloadIcon, Loader2Icon, PlusIcon } from "lucide-react";
22
import { useTranslation } from "react-i18next";
33
import type { RegistrySkill } from "@prompthub/shared/types";
44
import { SkillIcon } from "./SkillIcon";
5+
import { SkillVariantBadgeList } from "./SkillVariantBadgeList";
6+
import { buildSkillVariantBadges } from "../../services/skill-variant-badges";
57

68
const MAX_STAGGERED_STORE_CARDS = 12;
79

@@ -26,6 +28,10 @@ export function SkillStoreCard({
2628
}: SkillStoreCardProps) {
2729
const { t } = useTranslation();
2830
const isInstallingThis = installingSourceId === skill.source_id;
31+
const badges = buildSkillVariantBadges(skill, t, {
32+
hasUpdate,
33+
isInstalled,
34+
});
2935

3036
return (
3137
<div
@@ -52,6 +58,7 @@ export function SkillStoreCard({
5258
<p className="text-[11px] text-muted-foreground truncate mt-0.5">
5359
{skill.description}
5460
</p>
61+
<SkillVariantBadgeList badges={badges} className="mt-2 flex flex-wrap gap-1" />
5562
{skill.weekly_installs && (
5663
<div className="mt-1.5 inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary">
5764
{skill.weekly_installs}/wk

0 commit comments

Comments
 (0)