Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit 193fa56

Browse files
committed
FEA-1550: Normalize pack catalog IPC contract
- Map pack catalog SQL rows into the shared renderer CatalogEntry DTO before crossing IPC. - Update catalog contents, install runs, and install orchestration to consume the same camelCase catalog contract. - Guard Packs UI rendering against malformed catalog array fields so partial data cannot crash the screen. - Add focused catalog-store contract coverage for catalog, history, and install-run DTO mapping. Testing: Catalog-store contract test, desktop typecheck, desktop lint, and renderer production build passed. Risks: Low; catalog IPC now uses the documented shared DTO shape consistently.
1 parent 0252b13 commit 193fa56

7 files changed

Lines changed: 395 additions & 115 deletions

File tree

apps/desktop/src/main/agent-dashboard-design-system-runtime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ function registerDesignSystemDbIpcHandlers(
429429
ipcMain.handle("desktop:db:get-catalog-readme", withStoreDb(async (dbForStores, packId: unknown) => {
430430
if (typeof packId !== "string") return null;
431431
const entry = await catalogStore.getCatalog(dbForStores, packId);
432-
return entry?.readme_excerpt ?? null;
432+
return entry?.readmeExcerpt ?? null;
433433
}));
434434

435435
ipcMain.handle("desktop:db:get-catalog-contents", withStoreDb(async (dbForStores, packId: unknown) => {
@@ -438,7 +438,7 @@ function registerDesignSystemDbIpcHandlers(
438438
if (!entry) return null;
439439
await refreshCatalogContents(dbForStores, entry);
440440
const refreshed = await catalogStore.getCatalog(dbForStores, packId);
441-
return refreshed?.contents_cache ?? null;
441+
return refreshed?.contentsCache ?? null;
442442
}));
443443

444444
ipcMain.handle("desktop:db:get-catalog-history", withStoreDb((dbForStores, packId: unknown) => {

apps/desktop/src/main/packs/catalog-contents.ts

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* (claude-plugins-official entries)
1818
* - none — pack has no skill/command listing (RTK, claude-code-router)
1919
*
20-
* Returns [{ name, kind, description?, path? }]. kind is one of
20+
* Returns [{ name, type, description?, path? }]. type is one of
2121
* 'skill', 'command', 'agent', 'plugin'.
2222
*/
2323

@@ -44,7 +44,7 @@ export type ContentItemKind = "skill" | "command" | "agent" | "plugin";
4444

4545
export interface ContentItem {
4646
name: string;
47-
kind: ContentItemKind;
47+
type: ContentItemKind;
4848
description?: string | null;
4949
path?: string;
5050
category?: string;
@@ -78,10 +78,10 @@ interface ContentsSpec {
7878
}
7979

8080
export interface CatalogEntry {
81-
pack_id: string;
82-
github_url: string;
83-
contents: ContentsSpec | null;
84-
contents_fetched_at?: string | null;
81+
packId: string;
82+
githubUrl: string;
83+
contents: Record<string, unknown> | null;
84+
contentsFetchedAt?: string | null;
8585
}
8686

8787
interface GitHubTreeEntry {
@@ -107,6 +107,31 @@ type CatalogDb = DbClient;
107107

108108
// ---------- low-level GitHub helpers ----------
109109

110+
function readString(
111+
value: Record<string, unknown>,
112+
key: keyof ContentsSpec,
113+
): string | undefined {
114+
const raw = value[key];
115+
return typeof raw === "string" ? raw : undefined;
116+
}
117+
118+
function readStringArray(
119+
value: Record<string, unknown>,
120+
key: keyof ContentsSpec,
121+
): string[] {
122+
const raw = value[key];
123+
return Array.isArray(raw)
124+
? raw.filter((item): item is string => typeof item === "string")
125+
: [];
126+
}
127+
128+
function readKind(value: Record<string, unknown>): ContentItemKind | undefined {
129+
const raw = value.kind;
130+
return raw === "skill" || raw === "command" || raw === "agent" || raw === "plugin"
131+
? raw
132+
: undefined;
133+
}
134+
110135
function parseGithubUrl(url: string | null | undefined): ParsedRepo | null {
111136
const m = String(url || "").match(/github\.com[/:]([^/]+)\/([^/?#.]+)/);
112137
return m ? { owner: m[1], repo: m[2].replace(/\.git$/, "") } : null;
@@ -226,13 +251,13 @@ async function fetchSkillTree(
226251
`repos/${owner}/${repo}/contents/${encodeURI(entry.path)}/${skillMarker}`,
227252
);
228253
if (!file || !file.content) {
229-
skills.push({ name: entry.name, kind: "skill", path: entry.path });
254+
skills.push({ name: entry.name, type: "skill", path: entry.path });
230255
continue;
231256
}
232257
const meta = parseSkillFrontmatterFromBase64(file.content);
233258
skills.push({
234259
name: meta.name || entry.name,
235-
kind: "skill",
260+
type: "skill",
236261
description: meta.description || null,
237262
path: entry.path,
238263
});
@@ -259,7 +284,7 @@ async function fetchFlatMd(
259284
)
260285
.map((e) => ({
261286
name: e.name.replace(/\.md$/, ""),
262-
kind: kind || "command",
287+
type: kind || "command",
263288
path: e.path,
264289
}));
265290
}
@@ -288,7 +313,7 @@ async function fetchNestedMd(
288313
if (file.name.toLowerCase().startsWith("readme")) continue;
289314
items.push({
290315
name: file.name.replace(/\.md$/, ""),
291-
kind: kind || "agent",
316+
type: kind || "agent",
292317
category: cat.name,
293318
path: file.path,
294319
});
@@ -327,7 +352,7 @@ async function fetchClaudeMarketplace(
327352
if (Array.isArray(parsed.plugins)) {
328353
const items: ContentItem[] = parsed.plugins.map((p) => ({
329354
name: p.name,
330-
kind: "plugin" as const,
355+
type: "plugin" as const,
331356
description: p.description || null,
332357
}));
333358
// If a plugins_root is declared, walk each plugin's skills/ dir for a
@@ -416,7 +441,7 @@ async function fetchNestedSkillTree(
416441
if (skillDir.type !== "dir") continue;
417442
items.push({
418443
name: skillDir.name,
419-
kind: "skill",
444+
type: "skill",
420445
category: teamDir.name,
421446
path: skillDir.path,
422447
});
@@ -431,37 +456,54 @@ async function fetchNestedSkillTree(
431456

432457
export async function fetchContents(entry: CatalogEntry): Promise<ContentItem[]> {
433458
const contents = entry.contents;
434-
if (!contents || !contents.type) return [];
435-
const parsed = parseGithubUrl(entry.github_url);
459+
const type = contents ? readString(contents, "type") : undefined;
460+
if (!contents || !type) return [];
461+
const parsed = parseGithubUrl(entry.githubUrl);
436462
if (!parsed) return [];
437463
const { owner, repo } = parsed;
438464

439-
switch (contents.type) {
440-
case "github-skill-tree":
441-
return fetchSkillTree(owner, repo, contents.skills_path!, contents.skill_marker);
442-
case "github-multi-skill-tree":
443-
return fetchMultiSkillTree(owner, repo, contents.skill_paths, contents.skill_marker);
444-
case "github-flat-md":
445-
return fetchFlatMd(owner, repo, contents.md_path!, contents.kind);
446-
case "github-nested-md":
447-
return fetchNestedMd(owner, repo, contents.root_path!, contents.kind);
465+
switch (type) {
466+
case "github-skill-tree": {
467+
const skillsPath = readString(contents, "skills_path");
468+
if (!skillsPath) return [];
469+
return fetchSkillTree(owner, repo, skillsPath, readString(contents, "skill_marker"));
470+
}
471+
case "github-multi-skill-tree": {
472+
const skillPaths = readStringArray(contents, "skill_paths");
473+
if (skillPaths.length === 0) return [];
474+
return fetchMultiSkillTree(owner, repo, skillPaths, readString(contents, "skill_marker"));
475+
}
476+
case "github-flat-md": {
477+
const mdPath = readString(contents, "md_path");
478+
if (!mdPath) return [];
479+
return fetchFlatMd(owner, repo, mdPath, readKind(contents));
480+
}
481+
case "github-nested-md": {
482+
const rootPath = readString(contents, "root_path");
483+
if (!rootPath) return [];
484+
return fetchNestedMd(owner, repo, rootPath, readKind(contents));
485+
}
448486
case "github-nested-skill-tree":
449-
return fetchNestedSkillTree(owner, repo, contents.match_pattern);
487+
return fetchNestedSkillTree(owner, repo, readString(contents, "match_pattern"));
450488
case "claude-marketplace": {
451-
const repoFromContents = contents.marketplace_repo
452-
? parseGithubUrl(`https://github.com/${contents.marketplace_repo}`)
489+
const marketplaceRepo = readString(contents, "marketplace_repo");
490+
const repoFromContents = marketplaceRepo
491+
? parseGithubUrl(`https://github.com/${marketplaceRepo}`)
453492
: null;
454493
const mkO = repoFromContents ? repoFromContents.owner : owner;
455494
const mkR = repoFromContents ? repoFromContents.repo : repo;
456-
return fetchClaudeMarketplace(mkO, mkR, contents.plugins_root);
495+
return fetchClaudeMarketplace(mkO, mkR, readString(contents, "plugins_root"));
457496
}
458497
case "github-claude-plugin": {
459-
const repoFromContents = contents.marketplace_repo
460-
? parseGithubUrl(`https://github.com/${contents.marketplace_repo}`)
498+
const marketplaceRepo = readString(contents, "marketplace_repo");
499+
const repoFromContents = marketplaceRepo
500+
? parseGithubUrl(`https://github.com/${marketplaceRepo}`)
461501
: null;
462502
const pluginO = repoFromContents ? repoFromContents.owner : owner;
463503
const pluginR = repoFromContents ? repoFromContents.repo : repo;
464-
return fetchClaudePlugin(pluginO, pluginR, contents.plugin_path!);
504+
const pluginPath = readString(contents, "plugin_path");
505+
if (!pluginPath) return [];
506+
return fetchClaudePlugin(pluginO, pluginR, pluginPath);
465507
}
466508
case "none":
467509
return [];
@@ -479,13 +521,13 @@ export async function refreshCatalogContents(
479521
catalogEntry: CatalogEntry,
480522
): Promise<ContentItem[]> {
481523
const items = await fetchContents(catalogEntry);
482-
await applyContentsFetch(db, { pack_id: catalogEntry.pack_id, items });
524+
await applyContentsFetch(db, { pack_id: catalogEntry.packId, items });
483525
return items;
484526
}
485527

486528
export function isContentsFresh(entry: CatalogEntry): boolean {
487-
if (!entry.contents_fetched_at) return false;
529+
if (!entry.contentsFetchedAt) return false;
488530
return (
489-
Date.now() - new Date(entry.contents_fetched_at).getTime() < CONTENTS_TTL_MS
531+
Date.now() - new Date(entry.contentsFetchedAt).getTime() < CONTENTS_TTL_MS
490532
);
491533
}

0 commit comments

Comments
 (0)