@nospt/plugin-dev-ai-hub-common 1.1.3 → 1.2.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"resources.esm.js","sources":["../src/resources.ts"],"sourcesContent":["/**\n * The v2 read contract: AiResource vocabulary, the flat ResourceSummary\n * returned by the backend, and the shared framework resolver (ADR-0003).\n *\n * The legacy asset-model types in `types.ts` coexist with this module until\n * the legacy silo is deleted (slice 8, issue #34).\n */\n\n/** The six canonical `spec.type` values of an AiResource (ADR-0003, ADR-0010). */\nexport const RESOURCE_TYPES = [\n 'skill',\n 'agent',\n 'hook',\n 'mcp-config',\n 'plugin',\n 'marketplace',\n] as const;\n\nexport type ResourceType = (typeof RESOURCE_TYPES)[number];\n\nexport function isResourceType(value: unknown): value is ResourceType {\n return (\n typeof value === 'string' &&\n (RESOURCE_TYPES as readonly string[]).includes(value)\n );\n}\n\n/**\n * The AiResource types a `plugin` may contain — restricted to the component\n * kinds a real Claude Code / GitHub Copilot plugin manifest allows: `skills`,\n * `agents`, `hooks` and `mcpServers` (schemastore `claude-code-plugin` /\n * `claude-code-marketplace.json`). A plugin's other manifest components\n * (`commands`, `lspServers`, `outputStyles`, `themes`, `channels`, `monitors`)\n * have no AiResource type, so they are never members here; other plugins are\n * `dependencies`, not containment.\n */\nexport const PLUGIN_MEMBER_TYPES: readonly ResourceType[] = [\n 'skill',\n 'agent',\n 'hook',\n 'mcp-config',\n];\n\n/**\n * The AiResource types a `marketplace` may contain: `plugin` only. A real\n * `.claude-plugin/marketplace.json` `plugins` array lists plugins and nothing\n * else (required by the schema Claude Code and Copilot CLI both read).\n */\nexport const MARKETPLACE_MEMBER_TYPES: readonly ResourceType[] = ['plugin'];\n\n/**\n * Whether a container of `parent` type renders a child of `child` type as a\n * member — restricted to what a real Claude/Copilot plugin or marketplace\n * manifest permits (ADR-0010/0015). A `marketplace` contains `plugin` only; a\n * `plugin` contains the component types DevAI Hub models. Any other pairing is\n * silently not rendered. This is a rendering convention, not catalog\n * validation (upstream `allowedKinds` is the broad `[\"AiResource\"]`).\n */\nexport function isRenderableContainment(\n parent: ResourceType,\n child: ResourceType,\n): boolean {\n if (parent === 'marketplace') return MARKETPLACE_MEMBER_TYPES.includes(child);\n if (parent === 'plugin') return PLUGIN_MEMBER_TYPES.includes(child);\n return false;\n}\n\n/** Annotation prefix owned by DevAI Hub. */\nexport const DEVAIHUB_ANNOTATION_PREFIX = 'devaihub.io';\n\n/** Comma-separated compatible-framework tokens (non-skill read path, ADR-0003). */\nexport const ANNOTATION_COMPATIBLE_FRAMEWORKS = `${DEVAIHUB_ANNOTATION_PREFIX}/compatible-frameworks`;\n\n/** Optional free-form help text shown on the detail panel. */\nexport const ANNOTATION_HELP = `${DEVAIHUB_ANNOTATION_PREFIX}/help`;\n\n/** Known framework tokens; unknown tokens pass through as-is (ADR-0003). */\nexport const KNOWN_FRAMEWORKS = [\n 'github-copilot',\n 'claude-code',\n 'cursor',\n 'google-gemini',\n 'opencode',\n 'all',\n] as const;\n\nexport type FrameworkToken = (typeof KNOWN_FRAMEWORKS)[number];\n\nconst FRAMEWORK_ALIASES: Record<string, FrameworkToken> = {\n copilot: 'github-copilot',\n claude: 'claude-code',\n gemini: 'google-gemini',\n};\n\n/**\n * Normalise a framework token: trim, lowercase, resolve aliases\n * (`claude` → `claude-code`). Unknown tokens pass through literally.\n */\nexport function normalizeFramework(token: string): string {\n const t = token.trim().toLowerCase();\n return FRAMEWORK_ALIASES[t] ?? t;\n}\n\n/**\n * Narrow a resource's frameworks to a deployment-level allow-list\n * (`devAiHub.frameworks.allowed`). Omitting the config key (`allowed` is\n * `undefined`) permits every framework; an explicit empty list permits none,\n * so a deployment can suppress framework exposure entirely. Both sides are\n * normalised (aliases resolved) before comparison.\n *\n * The `all` wildcard is expanded to the allow-list itself before filtering, so\n * a resource that declares `all` degrades to \"every permitted host\" rather\n * than disappearing entirely.\n */\nexport function filterAllowedFrameworks(\n frameworks: string[],\n allowed: string[] | undefined,\n): string[] {\n if (allowed === undefined) {\n return frameworks;\n }\n const permitted = new Set(\n allowed.map(normalizeFramework).filter(f => f && f !== 'all'),\n );\n const expanded = frameworks.flatMap(f =>\n normalizeFramework(f) === 'all' ? [...permitted] : [normalizeFramework(f)],\n );\n return Array.from(new Set(expanded)).filter(f => permitted.has(f));\n}\n\n/**\n * The minimal structural shape of an AiResource entity that the read helpers\n * need. A real `@backstage/catalog-model` `Entity` satisfies it, but keeping\n * the shape local means `-common` (and therefore the frontend) never depends\n * on catalog packages (architecture.md).\n */\nexport interface AiResourceEntityLike {\n metadata: {\n annotations?: Record<string, string>;\n };\n spec?: {\n type?: unknown;\n /** Native compatible-frameworks field, honoured regardless of `spec.type`. */\n agents?: unknown;\n [key: string]: unknown;\n };\n}\n\n/**\n * Resolve the compatible frameworks of an AiResource (ADR-0003):\n * 1. a non-empty `spec.agents` → the native field, for any `spec.type`;\n * 2. otherwise → the `devaihub.io/compatible-frameworks` annotation\n * (also the fallback when `spec.agents` is empty/absent);\n * 3. otherwise → `[]`.\n * Tokens are normalised; unknown tokens pass through. `spec.agents` is\n * preferred over the annotation because it lives on the entity itself\n * rather than in an annotation namespace we may retire.\n */\nexport function getFrameworks(entity: AiResourceEntityLike): string[] {\n if (Array.isArray(entity.spec?.agents)) {\n const agents = entity.spec.agents\n .filter((a): a is string => typeof a === 'string')\n .map(normalizeFramework)\n .filter(Boolean);\n if (agents.length > 0) {\n return dedupe(agents);\n }\n }\n\n const raw = entity.metadata.annotations?.[ANNOTATION_COMPATIBLE_FRAMEWORKS];\n if (!raw) {\n return [];\n }\n return dedupe(raw.split(',').map(normalizeFramework).filter(Boolean));\n}\n\nfunction dedupe(tokens: string[]): string[] {\n return Array.from(new Set(tokens));\n}\n\n/**\n * Icon identifiers for the type→card registry. `-common` is imported by the\n * backend, so it stays React-free: the frontend maps these identifiers to\n * actual icon components.\n */\nexport type ResourceTypeIcon =\n | 'tools'\n | 'robot'\n | 'flash'\n | 'plug'\n | 'puzzle'\n | 'store';\n\nexport interface ResourceTypeInfo {\n type: ResourceType;\n /** Singular display label, e.g. \"Skill\" (card type caption). */\n label: string;\n /** Plural display label, e.g. \"Skills\" (stat tiles, filters). */\n pluralLabel: string;\n /** Icon identifier resolved to a component by the frontend. */\n icon: ResourceTypeIcon;\n /**\n * Colour role name; the frontend resolves it to the plugin-owned\n * `--devaihub-type-<role>` custom properties (ADR-0008, NOS palette).\n */\n colorRole: ResourceType;\n}\n\n/**\n * The typed type→card registry (ADR-0003/ADR-0008): producer vocabulary and\n * consumer rendering share this single source, so they cannot drift. Adding\n * a type is one entry here plus one CSS token pair in the frontend.\n */\nexport const RESOURCE_TYPE_REGISTRY: Record<ResourceType, ResourceTypeInfo> = {\n skill: {\n type: 'skill',\n label: 'Skill',\n pluralLabel: 'Skills',\n icon: 'tools',\n colorRole: 'skill',\n },\n agent: {\n type: 'agent',\n label: 'Agent',\n pluralLabel: 'Agents',\n icon: 'robot',\n colorRole: 'agent',\n },\n hook: {\n type: 'hook',\n label: 'Hook',\n pluralLabel: 'Hooks',\n icon: 'flash',\n colorRole: 'hook',\n },\n 'mcp-config': {\n type: 'mcp-config',\n label: 'MCP Config',\n pluralLabel: 'MCP Configs',\n icon: 'plug',\n colorRole: 'mcp-config',\n },\n plugin: {\n type: 'plugin',\n label: 'Plugin',\n pluralLabel: 'Plugins',\n icon: 'puzzle',\n colorRole: 'plugin',\n },\n marketplace: {\n type: 'marketplace',\n label: 'Marketplace',\n pluralLabel: 'Marketplaces',\n icon: 'store',\n colorRole: 'marketplace',\n },\n};\n\n/**\n * The flat contract the backend returns to the frontend. The frontend never\n * sees a raw Backstage `Entity` — only this (architecture.md).\n */\nexport interface ResourceSummary {\n entityRef: string;\n name: string;\n title?: string;\n description?: string;\n tags: string[];\n type: ResourceType;\n lifecycle: string;\n owner?: string;\n sourceLocation?: string;\n frameworks: string[];\n /**\n * True when an active deployment allow-list (`devAiHub.frameworks.allowed`)\n * left this resource with no frameworks — either it removed the ones the\n * author declared, or the allow-list itself is empty. Lets the UI tell\n * \"author declared nothing\" (`frameworks: []`, show neutral install\n * defaults) apart from \"the deployment suppressed them\" (hide generated\n * install paths and launchers; Copy/Download still work).\n */\n frameworksRestricted?: boolean;\n /**\n * The framework set that install-path and launcher generation must be\n * scoped to, when it differs from `frameworks`. Only set for a resource\n * that declared no frameworks (which otherwise means \"every host\") under an\n * active allow-list: it holds the allow-list itself, so a bare resource\n * cannot produce launchers for tools the deployment disallowed. Kept\n * separate from `frameworks` so this inferred set never renders as a\n * compatibility badge the author never claimed.\n */\n installFrameworks?: string[];\n version?: string;\n kind: string;\n /**\n * Entity refs this container declares as its contents — a `marketplace`'s\n * `spec.plugins`, a `plugin`'s `spec.skills`; empty for leaf types. Filtered\n * to the caller's visible set and to the child types the container renders\n * (ADR-0015). The frontend resolves each ref against the summaries it holds.\n */\n children: string[];\n /**\n * Entity refs of the containers that declare this resource — the in-memory\n * inverse of `children`, built over the same caller-visible catalog read\n * (ADR-0015). A resource can belong to several containers.\n */\n parents: string[];\n /** Count of `children` (their declared, caller-visible members). */\n childCount?: number;\n helpText?: string;\n annotations: Record<string, string>;\n}\n\nexport interface ResourceListResponse {\n items: ResourceSummary[];\n}\n\n/**\n * The shape of a resource's body (CONTEXT.md: bodies are type-shaped, not\n * uniformly markdown). Drives detail-panel rendering: markdown is rendered,\n * json is pretty-printed and syntax-highlighted (ADR-0014). `plugin` and\n * `marketplace` bodies are the real Claude Code `plugin.json` /\n * `marketplace.json` manifest — not a hand-authored pointer doc, superseding\n * ADR-0010's markdown-only body for `marketplace`.\n */\nexport type BodyShape = 'markdown' | 'json';\n\nexport function getBodyShape(type: ResourceType): BodyShape {\n return type === 'mcp-config' || type === 'plugin' || type === 'marketplace'\n ? 'json'\n : 'markdown';\n}\n\n/**\n * Whether the detail panel shows a \"Content\" section for this type.\n *\n * Suppressed for exactly the JSON-bodied types (`getBodyShape` === 'json'):\n * `plugin` and `marketplace` are containers whose value is the relationship\n * graph rather than the manifest, and an `mcp-config`'s body is a settings\n * fragment to merge — in all three the raw JSON buries the panel and is\n * better reached through Copy, Download or the install dialog, which is\n * where it is actionable. The markdown types keep their rendered body.\n */\nexport function showsContentSection(type: ResourceType): boolean {\n return getBodyShape(type) !== 'json';\n}\n\n/**\n * Whether downloading the body delivers the artifact itself. True for every\n * type: `plugin`/`marketplace` bodies are now the real manifest JSON, so a\n * download delivers that file itself rather than a non-actionable pointer\n * doc (ADR-0014, superseding ADR-0009's amendment and ADR-0010 on this\n * point).\n */\nexport function hasDownloadableArtifact(_type: ResourceType): boolean {\n return true;\n}\n\n/**\n * Whether copying the body to the clipboard is a meaningful install action.\n * True for every type: `marketplace`'s body is now the real manifest JSON,\n * not an instructions doc, so copying it is no longer a misrepresentation\n * (ADR-0014, superseding ADR-0010 on this point).\n */\nexport function hasCopyableBody(_type: ResourceType): boolean {\n return true;\n}\n\n/**\n * How a body reaches its install path.\n *\n * `drop-in` — the body becomes the file (or directory) at `path`, which is\n * the resource's own; writing it touches nothing else.\n *\n * `merge` — `path` is a shared settings file the user already owns, and the\n * body is a fragment to merge into it. The distinction is not cosmetic:\n * following a `merge` target as if it were a `drop-in` overwrites the user's\n * existing configuration, so the UI must never present the two identically.\n */\nexport type InstallMode = 'drop-in' | 'merge';\n\nexport interface ResourceInstallTarget {\n /** Workspace-relative path. */\n path: string;\n mode: InstallMode;\n}\n\n/**\n * The concrete framework tokens a body can be installed into — `all` is a\n * wildcard over these, never a destination itself.\n */\nexport const INSTALLABLE_FRAMEWORKS = [\n 'claude-code',\n 'github-copilot',\n 'google-gemini',\n 'cursor',\n 'opencode',\n] as const;\n\n/**\n * Convention table: (type, framework) → where the body goes and how.\n * A missing entry means the pair has no filesystem convention — `plugin` and\n * `marketplace` bodies are pointers that install through their framework\n * (ADR-0009, ADR-0010), so they carry install commands rather than paths.\n */\nconst INSTALL_PATHS: Record<\n ResourceType,\n Record<string, { mode: InstallMode; path: (name: string) => string }>\n> = {\n // Every host reads a skill as a self-contained directory. Copilot also\n // picks up `.claude/skills`, but each tool's own path is what we recommend;\n // `.agents/skills` is the cross-tool alias Copilot and Gemini both honour,\n // which makes it the right answer for an unrecognised framework.\n skill: {\n 'claude-code': { mode: 'drop-in', path: name => `.claude/skills/${name}/` },\n 'github-copilot': {\n mode: 'drop-in',\n path: name => `.github/skills/${name}/`,\n },\n 'google-gemini': {\n mode: 'drop-in',\n path: name => `.gemini/skills/${name}/`,\n },\n cursor: { mode: 'drop-in', path: name => `.cursor/skills/${name}/` },\n opencode: { mode: 'drop-in', path: name => `.opencode/skills/${name}/` },\n default: { mode: 'drop-in', path: name => `.agents/skills/${name}/` },\n },\n agent: {\n 'claude-code': {\n mode: 'drop-in',\n path: name => `.claude/agents/${name}.md`,\n },\n 'github-copilot': {\n mode: 'drop-in',\n path: name => `.github/agents/${name}.agent.md`,\n },\n 'google-gemini': {\n mode: 'drop-in',\n path: name => `.gemini/agents/${name}.md`,\n },\n cursor: { mode: 'drop-in', path: name => `.cursor/rules/${name}.mdc` },\n opencode: { mode: 'drop-in', path: name => `.opencode/agents/${name}.md` },\n default: { mode: 'drop-in', path: name => `.ai/agents/${name}.md` },\n },\n // All four hosts have a hook system, but only Copilot gives each hook its\n // own file; the rest register hooks inside a shared settings document.\n // OpenCode has no declarative hook config at all — hooks are exclusively\n // JS/TS plugin modules — so it deliberately carries no entry here, the same\n // way it carries no `default`.\n hook: {\n 'claude-code': { mode: 'merge', path: () => `.claude/settings.json` },\n 'github-copilot': {\n mode: 'drop-in',\n path: name => `.github/hooks/${name}.json`,\n },\n 'google-gemini': { mode: 'merge', path: () => `.gemini/settings.json` },\n cursor: { mode: 'merge', path: () => `.cursor/hooks.json` },\n },\n 'mcp-config': {\n 'claude-code': { mode: 'merge', path: () => `.mcp.json` },\n 'github-copilot': { mode: 'merge', path: () => `.vscode/mcp.json` },\n 'google-gemini': { mode: 'merge', path: () => `.gemini/settings.json` },\n cursor: { mode: 'merge', path: () => `.cursor/mcp.json` },\n opencode: { mode: 'merge', path: () => `opencode.json` },\n },\n plugin: {},\n marketplace: {},\n};\n\n/**\n * Where a resource's body installs for one framework, and whether that path\n * is the body's own file or a settings file to merge into. Returns\n * `undefined` when the (type, framework) pair has no filesystem convention.\n */\nexport function getResourceInstallTarget(\n type: ResourceType,\n framework: string,\n name: string,\n): ResourceInstallTarget | undefined {\n const conventions = INSTALL_PATHS[type];\n const entry =\n conventions[normalizeFramework(framework)] ?? conventions.default;\n return entry ? { path: entry.path(name), mode: entry.mode } : undefined;\n}\n\n/**\n * The frameworks to show install paths for. An empty list means the resource\n * declared no compatibility and gets the neutral `default` convention; `all`\n * is a wildcard and expands to every installable host, so it never collapses\n * to a single row — or, for types with no `default`, to no rows at all.\n */\nexport function expandInstallFrameworks(frameworks: string[]): string[] {\n const normalized = dedupe(frameworks.map(normalizeFramework));\n if (normalized.length === 0) {\n return ['default'];\n }\n if (normalized.includes('all')) {\n return [...INSTALLABLE_FRAMEWORKS];\n }\n return normalized;\n}\n\n/*\n * Marketplace journey helpers (ADR-0010). A marketplace installs in two\n * steps — register the catalog with the AI tool, then install plugins from\n * it. Both Claude Code and Copilot CLI read the same\n * `.claude-plugin/marketplace.json` format and address the repo as\n * `owner/repo`; the plugin derives that slug from the mandatory\n * `source-location` (the producer contract requires the body doc to live\n * inside the marketplace repo). No derivation → the UI falls back to the\n * rendered body, which stays canonical.\n */\n\n/** Frameworks with a marketplace-add concept (Cursor/Gemini have none). */\nexport const MARKETPLACE_CAPABLE_FRAMEWORKS = [\n 'claude-code',\n 'github-copilot',\n] as const;\n\n/**\n * Narrow a resource's declared frameworks to those a given journey can\n * actually launch. A deep link is a claim about compatibility: offering\n * \"Install in Claude\" for a resource that never declared `claude-code` tells\n * the user something untrue about the resource.\n *\n * `all` and an empty list both mean \"unrestricted\" and expand to every\n * capable host. Anything else is intersected, so a declared framework with no\n * handler (Gemini) — or an unknown token — yields no links rather than\n * silently widening to every host.\n */\nexport function resolveCapableFrameworks(\n declared: string[],\n capable: readonly string[],\n): string[] {\n const normalized = dedupe(declared.map(normalizeFramework));\n if (normalized.length === 0 || normalized.includes('all')) {\n return [...capable];\n }\n return normalized.filter(f => capable.includes(f));\n}\n\n/**\n * Derive the `owner/repo` slug from a marketplace's `source-location`.\n * GitHub URLs only — other hosts return `undefined` (body-only fallback).\n */\nexport function getMarketplaceRepoSlug(\n sourceLocation: string | undefined,\n): string | undefined {\n if (!sourceLocation) {\n return undefined;\n }\n let url: URL;\n try {\n url = new URL(sourceLocation.replace(/^url:/, ''));\n } catch {\n return undefined;\n }\n if (url.hostname !== 'github.com') {\n return undefined;\n }\n const [owner, repo] = url.pathname.split('/').filter(Boolean);\n if (!owner || !repo) {\n return undefined;\n }\n return `${owner}/${repo.replace(/\\.git$/, '')}`;\n}\n\n/**\n * A one-click launcher for an add command: a custom-scheme URL that opens\n * the host app with the command pre-filled in the prompt box. The host\n * never auto-executes it — the user reviews and presses Enter — and an\n * unregistered scheme is a harmless no-op, so these complement the copy\n * button rather than replace it.\n */\nexport interface ResourceDeepLink {\n /** Host application name; render as \"Add in <label>\" / \"Install in <label>\". */\n label: string;\n href: string;\n /**\n * The framework token this launcher installs into, for the `tool` column on\n * the `install` event it records (ADR-0011). Not 1:1 with `label`:\n * `github-copilot` offers both \"VS Code\" and \"VS Code Insiders\".\n *\n * Optional only to keep this published interface additive — every link the\n * generators below build sets it, which `resources.test.ts` pins. A caller\n * reading a generated link can rely on it; one constructing its own shape\n * is not forced to supply it.\n */\n framework?: string;\n}\n\nexport interface MarketplaceAddCommand {\n framework: string;\n command: string;\n deepLinks: ResourceDeepLink[];\n}\n\n/**\n * The copyable marketplace-add command per compatible framework. `all`,\n * unknown-only, or empty framework lists expand to every capable framework;\n * frameworks without a marketplace concept produce no row.\n *\n * Claude Code carries a single deep link on the CLI's documented\n * `claude-cli://open?q=` handler, which opens a Claude Code terminal\n * session with the command pre-filled regardless of the user's editor.\n *\n * Copilot's CLI still has no URI scheme, but Copilot's *other* surface does:\n * VS Code 1.113 ships `vscode://chat-plugin/add-marketplace?ref=`, which\n * accepts a plain or base64 `owner/repo` and always shows a confirmation\n * dialog before registering the marketplace (it also dedupes against the\n * user's existing entries). So the Copilot row keeps its terminal command for\n * CLI users *and* gains editor launchers, mirroring how `mcp-config` already\n * maps `github-copilot` onto the VS Code stable/Insiders pair.\n */\nexport function getMarketplaceAddCommands(\n frameworks: string[],\n repoSlug: string,\n): MarketplaceAddCommand[] {\n const capable = resolveCapableFrameworks(\n frameworks,\n MARKETPLACE_CAPABLE_FRAMEWORKS,\n );\n const commands: MarketplaceAddCommand[] = [];\n for (const framework of capable) {\n if (framework === 'claude-code') {\n const command = `/plugin marketplace add ${repoSlug}`;\n commands.push({\n framework,\n command,\n deepLinks: [\n {\n label: 'Claude',\n href: `claude-cli://open?q=${encodeURIComponent(command)}`,\n framework,\n },\n ],\n });\n } else if (framework === 'github-copilot') {\n const ref = encodeURIComponent(repoSlug);\n commands.push({\n framework,\n command: `copilot plugin marketplace add ${repoSlug}`,\n deepLinks: [\n {\n label: 'VS Code',\n href: `vscode://chat-plugin/add-marketplace?ref=${ref}`,\n framework,\n },\n {\n label: 'VS Code Insiders',\n href: `vscode-insiders://chat-plugin/add-marketplace?ref=${ref}`,\n framework,\n },\n ],\n });\n }\n }\n return commands;\n}\n\n/** The raw-file URL for a GitHub blob `source-location`; else `undefined`. */\nfunction getRawGithubFileUrl(\n sourceLocation: string | undefined,\n): string | undefined {\n if (!sourceLocation) {\n return undefined;\n }\n let url: URL;\n try {\n url = new URL(sourceLocation.replace(/^url:/, ''));\n } catch {\n return undefined;\n }\n if (url.hostname !== 'github.com') {\n return undefined;\n }\n const [owner, repo, marker, ref, ...path] = url.pathname\n .split('/')\n .filter(Boolean);\n if (!owner || !repo || marker !== 'blob' || !ref || path.length === 0) {\n return undefined;\n }\n return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path.join(\n '/',\n )}`;\n}\n\n/**\n * A launcher that opens an agent with an install prompt pre-filled. Neither\n * host auto-executes: the user reads the prompt and presses Enter. Claude Code\n * and Cursor are the two tools exposing a generic prompt URI — Copilot's\n * editor has purpose-built routes but no prompt handler, and Gemini has\n * neither (its `gemini.google.com/app?prompt=` link drives the web app, which\n * cannot write to a local workspace).\n *\n * Cursor truncates a deeplink at the first raw `&`, so the prompt must be\n * percent-encoded — `encodeURIComponent` handles that. Cursor also caps the\n * URL at ~8000 characters; these prompts are a URL plus a path, far under it.\n */\nfunction claudePromptLink(prompt: string): ResourceDeepLink {\n return {\n label: 'Claude',\n href: `claude-cli://open?q=${encodeURIComponent(prompt)}`,\n framework: 'claude-code',\n };\n}\n\nfunction cursorPromptLink(prompt: string): ResourceDeepLink {\n return {\n label: 'Cursor',\n href: `cursor://anysphere.cursor-deeplink/prompt?text=${encodeURIComponent(\n prompt,\n )}`,\n framework: 'cursor',\n };\n}\n\n/**\n * Frameworks with a one-click agent-install handler (Gemini has none).\n * OpenCode's Desktop app does register a real `opencode://` scheme, but every\n * route (`open-project`, `new-session`) requires an absolute local\n * `directory` param a web page cannot supply, so it's excluded here too.\n */\nexport const AGENT_LINK_CAPABLE_FRAMEWORKS = [\n 'claude-code',\n 'github-copilot',\n 'cursor',\n] as const;\n\n/** Hosts reachable by a generic prompt URI — the only route for skill/hook. */\nexport const PROMPT_LINK_CAPABLE_FRAMEWORKS = [\n 'claude-code',\n 'cursor',\n] as const;\n\n/**\n * The plain http(s) URL behind a `source-location`. Unlike\n * `getRawGithubFileUrl` this accepts **directory (`tree`) URLs**, which is what\n * a multi-file `skill` actually carries — a raw-file URL would resolve to\n * `undefined` for exactly the type that most needs a launcher.\n */\nfunction getSourceUrl(sourceLocation: string | undefined): string | undefined {\n if (!sourceLocation) {\n return undefined;\n }\n try {\n const url = new URL(sourceLocation.replace(/^url:/, ''));\n return url.protocol === 'http:' || url.protocol === 'https:'\n ? url.toString()\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * One-click launchers for the types with no purpose-built install route —\n * `skill` and `hook`. Both ride the generic prompt handlers, so the prompt\n * names the *host's own* install path and respects its install mode: a `merge`\n * target is asked to be merged into, never overwritten, so a launcher can\n * never cost the user their existing settings.\n *\n * Returns `[]` when the source URL cannot be derived, or for hosts with no\n * prompt route (Copilot, Gemini) — the path list and copy/download remain.\n */\nexport function getPromptInstallLinks(\n type: ResourceType,\n sourceLocation: string | undefined,\n name: string,\n frameworks: string[] = [],\n): ResourceDeepLink[] {\n return getInstallSteps(type, sourceLocation, name, frameworks)\n .map(step => step.link)\n .filter((link): link is ResourceDeepLink => !!link);\n}\n\n/**\n * Everything a single host needs to install a resource: where the body goes,\n * the prompt that puts it there, and a launcher when the host has a URI to\n * launch.\n *\n * `link` is absent for hosts with no prompt route — Copilot and Gemini. That\n * absence is why the step carries `prompt` as text: a host without a URI is\n * not a host without an install path, and the same instruction that a launcher\n * would pre-fill can be pasted into that agent by hand. Every declared\n * framework therefore gets something actionable, not a bare path.\n */\nexport interface ResourceInstallStep {\n framework: string;\n target: ResourceInstallTarget;\n /** The install instruction, ready to run in that host's agent. */\n prompt: string;\n /** One-click launcher, where the host exposes a generic prompt URI. */\n link?: ResourceDeepLink;\n}\n\nexport function getInstallSteps(\n type: ResourceType,\n sourceLocation: string | undefined,\n name: string,\n frameworks: string[] = [],\n): ResourceInstallStep[] {\n const sourceUrl = getSourceUrl(sourceLocation);\n if (!sourceUrl) {\n return [];\n }\n const label = RESOURCE_TYPE_REGISTRY[type].label.toLowerCase();\n const steps: ResourceInstallStep[] = [];\n for (const framework of expandInstallFrameworks(frameworks)) {\n const target = getResourceInstallTarget(type, framework, name);\n if (!target) {\n continue;\n }\n const prompt =\n target.mode === 'merge'\n ? `Install the \"${name}\" ${label}: fetch ${sourceUrl} and merge it into ${target.path}, keeping my existing settings intact.`\n : `Install the \"${name}\" ${label}: fetch ${sourceUrl} and save it to ${target.path}.`;\n let link: ResourceDeepLink | undefined;\n if (framework === 'claude-code') {\n link = claudePromptLink(prompt);\n } else if (framework === 'cursor') {\n link = cursorPromptLink(prompt);\n }\n steps.push({ framework, target, prompt, link });\n }\n return steps;\n}\n\n/** Frameworks with a one-click MCP-install handler (Gemini has none). */\nexport const MCP_LINK_CAPABLE_FRAMEWORKS = [\n 'claude-code',\n 'github-copilot',\n 'cursor',\n] as const;\n\n/**\n * One-click install links for an `agent` resource, restricted to the\n * resource's own compatible frameworks. VS Code stable and Insiders use the\n * native `chat-agent/install?url=` handler (the mechanism behind\n * awesome-copilot's Install buttons) and are Copilot's hosts, so they follow\n * `github-copilot`: VS Code downloads the file at `url` and asks the user\n * where to save it. Claude Code uses the `claude-cli://open?q=` handler with\n * an install prompt pre-filled to the convention path — reviewed and sent by\n * the user, never auto-executed. All derive from the GitHub blob URL in\n * `source-location`; anything else returns `[]` (copy/download fallback).\n */\nexport function getAgentInstallLinks(\n sourceLocation: string | undefined,\n name: string,\n frameworks: string[] = [],\n): ResourceDeepLink[] {\n const rawUrl = getRawGithubFileUrl(sourceLocation);\n if (!rawUrl) {\n return [];\n }\n const capable = resolveCapableFrameworks(\n frameworks,\n AGENT_LINK_CAPABLE_FRAMEWORKS,\n );\n const encodedRawUrl = encodeURIComponent(rawUrl);\n const installPrompt = (framework: string) => {\n const target = getResourceInstallTarget('agent', framework, name);\n return `Install this agent: fetch ${rawUrl} and save it to ${target?.path}`;\n };\n const links: ResourceDeepLink[] = [];\n for (const framework of capable) {\n if (framework === 'claude-code') {\n links.push(claudePromptLink(installPrompt('claude-code')));\n } else if (framework === 'cursor') {\n links.push(cursorPromptLink(installPrompt('cursor')));\n } else if (framework === 'github-copilot') {\n links.push({\n label: 'VS Code',\n href: `vscode:chat-agent/install?url=${encodedRawUrl}`,\n framework,\n });\n links.push({\n label: 'VS Code Insiders',\n href: `vscode-insiders:chat-agent/install?url=${encodedRawUrl}`,\n framework,\n });\n }\n }\n return links;\n}\n\n/**\n * One-click install links for an `mcp-config` resource, derived from its body —\n * the canonical `.mcp.json` snippet (spec §3.4). VS Code carries a native\n * `vscode:mcp/install?{json}` handler (Insiders scheme twin), Cursor a\n * documented `cursor://anysphere.cursor-deeplink/mcp/install` one, and\n * Claude Code gets a `claude-cli://open?q=` prompt around\n * `claude mcp add-json` — pre-filled, reviewed, never auto-executed.\n * Hosts follow the resource's own compatible frameworks: `all`/empty expand\n * to every capable host, anything else is intersected, so an mcp-config\n * declaring only Gemini (no handler) gets no links rather than every host's.\n * A missing, unparseable, or multi-server body returns `[]` (copy fallback) —\n * the body stays canonical, links are conveniences derived from it.\n */\nexport function getMcpInstallLinks(\n frameworks: string[],\n resourceName: string,\n bodyContent: string | undefined,\n): ResourceDeepLink[] {\n if (!bodyContent) {\n return [];\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(bodyContent);\n } catch {\n return [];\n }\n if (typeof parsed !== 'object' || parsed === null) {\n return [];\n }\n\n let serverName = resourceName;\n let config: Record<string, unknown>;\n const servers = (parsed as { mcpServers?: unknown }).mcpServers;\n if (servers !== undefined) {\n if (typeof servers !== 'object' || servers === null) {\n return [];\n }\n const entries = Object.entries(servers as Record<string, unknown>);\n if (\n entries.length !== 1 ||\n typeof entries[0][1] !== 'object' ||\n entries[0][1] === null\n ) {\n return [];\n }\n serverName = entries[0][0];\n config = entries[0][1] as Record<string, unknown>;\n } else if ('command' in parsed || 'url' in parsed) {\n config = parsed as Record<string, unknown>;\n } else {\n return [];\n }\n\n const capable = resolveCapableFrameworks(\n frameworks,\n MCP_LINK_CAPABLE_FRAMEWORKS,\n );\n\n const configJson = JSON.stringify(config);\n const links: ResourceDeepLink[] = [];\n for (const framework of capable) {\n if (framework === 'claude-code') {\n const prompt = encodeURIComponent(\n `Install this MCP config by running: claude mcp add-json ${serverName} '${configJson}'`,\n );\n links.push({\n label: 'Claude',\n href: `claude-cli://open?q=${prompt}`,\n framework,\n });\n } else if (framework === 'github-copilot') {\n const vsConfig = encodeURIComponent(\n JSON.stringify({ name: serverName, ...config }),\n );\n links.push({\n label: 'VS Code',\n href: `vscode:mcp/install?${vsConfig}`,\n framework,\n });\n links.push({\n label: 'VS Code Insiders',\n href: `vscode-insiders:mcp/install?${vsConfig}`,\n framework,\n });\n } else if (framework === 'cursor') {\n try {\n links.push({\n label: 'Cursor',\n href: `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(\n serverName,\n )}&config=${btoa(configJson)}`,\n framework,\n });\n } catch {\n // btoa rejects non-Latin1 config content — skip the Cursor link.\n }\n }\n }\n return links;\n}\n\n/**\n * Step two of the journey: how to install a plugin from the marketplace.\n * `marketplaceName` is the entity name, which the producer contract requires\n * to equal the `name` in `marketplace.json`.\n */\nexport function getMarketplaceInstallTemplate(marketplaceName: string): string {\n return `/plugin install <plugin>@${marketplaceName}`;\n}\n\n/**\n * The `.claude/settings.json` snippet a team commits so collaborators are\n * prompted to install the marketplace automatically (Claude Code).\n */\nexport function getMarketplaceTeamSnippet(\n marketplaceName: string,\n repoSlug: string,\n): string {\n return JSON.stringify(\n {\n extraKnownMarketplaces: {\n [marketplaceName]: { source: { source: 'github', repo: repoSlug } },\n },\n },\n null,\n 2,\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resources.esm.js","sources":["../src/resources.ts"],"sourcesContent":["/**\n * The v2 read contract: AiResource vocabulary, the flat ResourceSummary\n * returned by the backend, and the shared framework resolver (ADR-0003).\n *\n * The legacy asset-model types in `types.ts` coexist with this module until\n * the legacy silo is deleted (slice 8, issue #34).\n */\n\n/** The six canonical `spec.type` values of an AiResource (ADR-0003, ADR-0010). */\nexport const RESOURCE_TYPES = [\n 'skill',\n 'agent',\n 'hook',\n 'mcp-config',\n 'plugin',\n 'marketplace',\n] as const;\n\nexport type ResourceType = (typeof RESOURCE_TYPES)[number];\n\nexport function isResourceType(value: unknown): value is ResourceType {\n return (\n typeof value === 'string' &&\n (RESOURCE_TYPES as readonly string[]).includes(value)\n );\n}\n\n/**\n * The AiResource types a `plugin` may contain — restricted to the component\n * kinds a real Claude Code / GitHub Copilot plugin manifest allows: `skills`,\n * `agents`, `hooks` and `mcpServers` (schemastore `claude-code-plugin` /\n * `claude-code-marketplace.json`). A plugin's other manifest components\n * (`commands`, `lspServers`, `outputStyles`, `themes`, `channels`, `monitors`)\n * have no AiResource type, so they are never members here; other plugins are\n * `dependencies`, not containment.\n */\nexport const PLUGIN_MEMBER_TYPES: readonly ResourceType[] = [\n 'skill',\n 'agent',\n 'hook',\n 'mcp-config',\n];\n\n/**\n * The AiResource types a `marketplace` may contain: `plugin` only. A real\n * `.claude-plugin/marketplace.json` `plugins` array lists plugins and nothing\n * else (required by the schema Claude Code and Copilot CLI both read).\n */\nexport const MARKETPLACE_MEMBER_TYPES: readonly ResourceType[] = ['plugin'];\n\n/**\n * Whether a container of `parent` type renders a child of `child` type as a\n * member — restricted to what a real Claude/Copilot plugin or marketplace\n * manifest permits (ADR-0010/0015). A `marketplace` contains `plugin` only; a\n * `plugin` contains the component types DevAI Hub models. Any other pairing is\n * silently not rendered. This is a rendering convention, not catalog\n * validation (upstream `allowedKinds` is the broad `[\"AiResource\"]`).\n */\nexport function isRenderableContainment(\n parent: ResourceType,\n child: ResourceType,\n): boolean {\n if (parent === 'marketplace') return MARKETPLACE_MEMBER_TYPES.includes(child);\n if (parent === 'plugin') return PLUGIN_MEMBER_TYPES.includes(child);\n return false;\n}\n\n/** Annotation prefix owned by DevAI Hub. */\nexport const DEVAIHUB_ANNOTATION_PREFIX = 'devaihub.io';\n\n/** Comma-separated compatible-framework tokens (non-skill read path, ADR-0003). */\nexport const ANNOTATION_COMPATIBLE_FRAMEWORKS = `${DEVAIHUB_ANNOTATION_PREFIX}/compatible-frameworks`;\n\n/** Optional free-form help text shown on the detail panel. */\nexport const ANNOTATION_HELP = `${DEVAIHUB_ANNOTATION_PREFIX}/help`;\n\n/** Known framework tokens; unknown tokens pass through as-is (ADR-0003). */\nexport const KNOWN_FRAMEWORKS = [\n 'github-copilot',\n 'claude-code',\n 'cursor',\n 'google-gemini',\n 'opencode',\n 'all',\n] as const;\n\nexport type FrameworkToken = (typeof KNOWN_FRAMEWORKS)[number];\n\nconst FRAMEWORK_ALIASES: Record<string, FrameworkToken> = {\n copilot: 'github-copilot',\n claude: 'claude-code',\n gemini: 'google-gemini',\n};\n\n/**\n * Normalise a framework token: trim, lowercase, resolve aliases\n * (`claude` → `claude-code`). Unknown tokens pass through literally.\n */\nexport function normalizeFramework(token: string): string {\n const t = token.trim().toLowerCase();\n return FRAMEWORK_ALIASES[t] ?? t;\n}\n\n/**\n * Narrow a resource's frameworks to a deployment-level allow-list\n * (`devAiHub.frameworks.allowed`, unioned with the `custom` keys). Omitting\n * the config key (`allowed` is `undefined`) permits every framework; an\n * explicit empty list permits none, so a deployment can suppress framework\n * exposure entirely. Both sides are normalised (aliases resolved) before\n * comparison.\n *\n * The `all` wildcard is expanded to the allow-list itself before filtering, so\n * a resource that declares `all` degrades to \"every permitted host\" rather\n * than disappearing entirely — minus `custom`, which `all` never reaches\n * (ADR-0018): the wildcard means \"every host the plugin knows\", and claiming\n * an internal assistant on every universal resource would be an assertion the\n * author never made.\n */\nexport function filterAllowedFrameworks(\n frameworks: string[],\n allowed: string[] | undefined,\n custom: readonly string[] = [],\n): string[] {\n if (allowed === undefined) {\n return frameworks;\n }\n const permitted = new Set(\n allowed.map(normalizeFramework).filter(f => f && f !== 'all'),\n );\n const customTokens = new Set(custom.map(normalizeFramework));\n const wildcard = [...permitted].filter(f => !customTokens.has(f));\n const expanded = frameworks.flatMap(f =>\n normalizeFramework(f) === 'all' ? wildcard : [normalizeFramework(f)],\n );\n return Array.from(new Set(expanded)).filter(f => permitted.has(f));\n}\n\n/**\n * A deployment-registered framework (`devAiHub.frameworks.custom`) — a tool\n * the plugin does not ship knowledge of, named by the token its authors write\n * in `spec.agents` (ADR-0018).\n */\nexport interface CustomFramework {\n /** Normalised token matched against a resource's declared frameworks. */\n name: string;\n /** Display label. Falls back to `name` when the config omits `displayName`. */\n displayName: string;\n /** Browser-loadable icon source; `undefined` selects the neutral fallback. */\n icon?: string;\n}\n\n/** The raw per-token shape of `devAiHub.frameworks.custom`. */\nexport interface CustomFrameworkConfig {\n displayName?: string;\n icon?: string;\n}\n\nexport interface ParsedCustomFrameworks {\n frameworks: CustomFramework[];\n /**\n * Human-readable reasons entries were dropped. The backend logs these; the\n * frontend ignores them, but both run the same parse so they can never\n * disagree about which tokens exist.\n */\n warnings: string[];\n}\n\n/**\n * Parse `devAiHub.frameworks.custom` into the registry both sides read.\n *\n * Entries are dropped rather than rejected wholesale — one malformed entry\n * must not take out a deployment's whole framework registry — and each drop\n * carries a reason:\n *\n * - a key that normalises to empty has nothing to match against;\n * - a key that collides with a built-in token or alias (`claude-code`,\n * `claude`) is ignored so built-ins stay authoritative: custom frameworks\n * are install-suppressed, and letting one shadow `claude-code` would\n * silently strip Claude's install paths;\n * - a duplicate after normalisation keeps the first entry, since the second\n * would otherwise win non-deterministically.\n */\nexport function parseCustomFrameworks(raw: unknown): ParsedCustomFrameworks {\n const frameworks: CustomFramework[] = [];\n const warnings: string[] = [];\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n return { frameworks, warnings };\n }\n\n const builtIn = new Set<string>(KNOWN_FRAMEWORKS);\n const seen = new Set<string>();\n\n for (const [key, value] of Object.entries(raw)) {\n const name = normalizeFramework(key);\n if (!name) {\n warnings.push(\n `devAiHub.frameworks.custom: ignored an entry with an empty name`,\n );\n continue;\n }\n if (builtIn.has(name)) {\n warnings.push(\n `devAiHub.frameworks.custom: ignored \"${key}\" — it resolves to the built-in framework \"${name}\"`,\n );\n continue;\n }\n if (seen.has(name)) {\n warnings.push(\n `devAiHub.frameworks.custom: ignored duplicate entry \"${key}\" — \"${name}\" is already registered`,\n );\n continue;\n }\n seen.add(name);\n\n const entry = (\n typeof value === 'object' && value !== null ? value : {}\n ) as CustomFrameworkConfig;\n const displayName =\n typeof entry.displayName === 'string' && entry.displayName.trim()\n ? entry.displayName.trim()\n : name;\n const icon =\n typeof entry.icon === 'string' && entry.icon.trim()\n ? entry.icon.trim()\n : undefined;\n frameworks.push({ name, displayName, icon });\n }\n\n return { frameworks, warnings };\n}\n\n/**\n * The allow-list actually applied, once `custom` is taken into account\n * (ADR-0018). Registering a custom framework activates the gate even when\n * `allowed` is omitted — otherwise the deployment would have registered a\n * token that nothing distinguishes from the unknown tokens beside it.\n *\n * - no custom frameworks → `allowed` unchanged (including `undefined`);\n * - `allowed` set → `allowed` ∪ custom;\n * - `allowed` omitted → every built-in ∪ custom.\n */\nexport function resolveAllowedFrameworks(\n allowed: string[] | undefined,\n custom: readonly string[],\n): string[] | undefined {\n if (custom.length === 0) {\n return allowed;\n }\n const base =\n allowed ?? KNOWN_FRAMEWORKS.filter((f): f is FrameworkToken => f !== 'all');\n return dedupe([...base, ...custom].map(normalizeFramework).filter(Boolean));\n}\n\n/**\n * The minimal structural shape of an AiResource entity that the read helpers\n * need. A real `@backstage/catalog-model` `Entity` satisfies it, but keeping\n * the shape local means `-common` (and therefore the frontend) never depends\n * on catalog packages (architecture.md).\n */\nexport interface AiResourceEntityLike {\n metadata: {\n annotations?: Record<string, string>;\n };\n spec?: {\n type?: unknown;\n /** Native compatible-frameworks field, honoured regardless of `spec.type`. */\n agents?: unknown;\n [key: string]: unknown;\n };\n}\n\n/**\n * Resolve the compatible frameworks of an AiResource (ADR-0003):\n * 1. a non-empty `spec.agents` → the native field, for any `spec.type`;\n * 2. otherwise → the `devaihub.io/compatible-frameworks` annotation\n * (also the fallback when `spec.agents` is empty/absent);\n * 3. otherwise → `[]`.\n * Tokens are normalised; unknown tokens pass through. `spec.agents` is\n * preferred over the annotation because it lives on the entity itself\n * rather than in an annotation namespace we may retire.\n */\nexport function getFrameworks(entity: AiResourceEntityLike): string[] {\n if (Array.isArray(entity.spec?.agents)) {\n const agents = entity.spec.agents\n .filter((a): a is string => typeof a === 'string')\n .map(normalizeFramework)\n .filter(Boolean);\n if (agents.length > 0) {\n return dedupe(agents);\n }\n }\n\n const raw = entity.metadata.annotations?.[ANNOTATION_COMPATIBLE_FRAMEWORKS];\n if (!raw) {\n return [];\n }\n return dedupe(raw.split(',').map(normalizeFramework).filter(Boolean));\n}\n\nfunction dedupe(tokens: string[]): string[] {\n return Array.from(new Set(tokens));\n}\n\n/**\n * Icon identifiers for the type→card registry. `-common` is imported by the\n * backend, so it stays React-free: the frontend maps these identifiers to\n * actual icon components.\n */\nexport type ResourceTypeIcon =\n | 'tools'\n | 'robot'\n | 'flash'\n | 'plug'\n | 'puzzle'\n | 'store';\n\nexport interface ResourceTypeInfo {\n type: ResourceType;\n /** Singular display label, e.g. \"Skill\" (card type caption). */\n label: string;\n /** Plural display label, e.g. \"Skills\" (stat tiles, filters). */\n pluralLabel: string;\n /** Icon identifier resolved to a component by the frontend. */\n icon: ResourceTypeIcon;\n /**\n * Colour role name; the frontend resolves it to the plugin-owned\n * `--devaihub-type-<role>` custom properties (ADR-0008, NOS palette).\n */\n colorRole: ResourceType;\n}\n\n/**\n * The typed type→card registry (ADR-0003/ADR-0008): producer vocabulary and\n * consumer rendering share this single source, so they cannot drift. Adding\n * a type is one entry here plus one CSS token pair in the frontend.\n */\nexport const RESOURCE_TYPE_REGISTRY: Record<ResourceType, ResourceTypeInfo> = {\n skill: {\n type: 'skill',\n label: 'Skill',\n pluralLabel: 'Skills',\n icon: 'tools',\n colorRole: 'skill',\n },\n agent: {\n type: 'agent',\n label: 'Agent',\n pluralLabel: 'Agents',\n icon: 'robot',\n colorRole: 'agent',\n },\n hook: {\n type: 'hook',\n label: 'Hook',\n pluralLabel: 'Hooks',\n icon: 'flash',\n colorRole: 'hook',\n },\n 'mcp-config': {\n type: 'mcp-config',\n label: 'MCP Config',\n pluralLabel: 'MCP Configs',\n icon: 'plug',\n colorRole: 'mcp-config',\n },\n plugin: {\n type: 'plugin',\n label: 'Plugin',\n pluralLabel: 'Plugins',\n icon: 'puzzle',\n colorRole: 'plugin',\n },\n marketplace: {\n type: 'marketplace',\n label: 'Marketplace',\n pluralLabel: 'Marketplaces',\n icon: 'store',\n colorRole: 'marketplace',\n },\n};\n\n/**\n * The flat contract the backend returns to the frontend. The frontend never\n * sees a raw Backstage `Entity` — only this (architecture.md).\n */\nexport interface ResourceSummary {\n entityRef: string;\n name: string;\n title?: string;\n description?: string;\n tags: string[];\n type: ResourceType;\n lifecycle: string;\n owner?: string;\n sourceLocation?: string;\n frameworks: string[];\n /**\n * True when this resource has no generated install path to offer — either\n * an active deployment allow-list (`devAiHub.frameworks.allowed`) left it\n * with no frameworks, or every framework it does have is a custom,\n * discovery-only one (ADR-0018). Lets the UI tell \"author declared nothing\"\n * (`frameworks: []`, show neutral install defaults) apart from \"nothing\n * installable remains\" (hide generated install paths and launchers;\n * Copy/Download still work).\n */\n frameworksRestricted?: boolean;\n /**\n * The framework set that install-path and launcher generation must be\n * scoped to, when it differs from `frameworks`. Two cases set it: a\n * resource that declared no frameworks (which otherwise means \"every host\")\n * under an active allow-list, where it holds the allow-list itself so a\n * bare resource cannot produce launchers for disallowed tools; and a\n * resource carrying custom frameworks, where it holds the remainder once\n * those are removed (ADR-0018). Kept separate from `frameworks` so neither\n * inferred set renders as a compatibility badge the author never claimed.\n */\n installFrameworks?: string[];\n version?: string;\n kind: string;\n /**\n * Entity refs this container declares as its contents — a `marketplace`'s\n * `spec.plugins`, a `plugin`'s `spec.skills`; empty for leaf types. Filtered\n * to the caller's visible set and to the child types the container renders\n * (ADR-0015). The frontend resolves each ref against the summaries it holds.\n */\n children: string[];\n /**\n * Entity refs of the containers that declare this resource — the in-memory\n * inverse of `children`, built over the same caller-visible catalog read\n * (ADR-0015). A resource can belong to several containers.\n */\n parents: string[];\n /** Count of `children` (their declared, caller-visible members). */\n childCount?: number;\n helpText?: string;\n annotations: Record<string, string>;\n /**\n * True when the resource is recommended from either source: its catalog\n * tags carry `RECOMMENDED_TAG` (set by `toResourceSummary`, ADR-0020), or\n * the backend router overlaid it from the `curation` table (ADR-0017).\n * `undefined` and `false` both mean \"not recommended\"; only `true` renders\n * the badge.\n */\n recommended?: boolean;\n /**\n * True when `recommended` comes from the entity's own `RECOMMENDED_TAG`\n * (ADR-0020). Such a recommendation is owned by the resource's author, so\n * the backoffice cannot remove it — only removing the tag can.\n */\n recommendedByTag?: boolean;\n /**\n * True when a curator recommended this resource in the backoffice — a\n * `curation` row (ADR-0017). Can be true alongside `recommendedByTag`, for\n * a row recorded before the author added the tag; the backoffice then\n * offers to clear it, so that removing the tag really un-recommends.\n */\n recommendedByCuration?: boolean;\n /**\n * When the plugin first saw this resource, as an ISO timestamp — set by the\n * backend router from its own `resource_first_seen` table (ADR-0021), since\n * the catalog exposes no creation date. `undefined` for a resource that\n * already existed when tracking began; the \"Newest\" sort ranks those as\n * the oldest.\n */\n firstSeenAt?: string;\n /**\n * Set by the backend router from the `curation` table (ADR-0019), and only\n * ever present when the caller explicitly asked for excluded resources via\n * `?includeExcluded=true`. The default `GET /resources` response omits\n * excluded resources entirely rather than flagging them, so on the browse\n * page this field is always `undefined`.\n */\n excluded?: boolean;\n /** Who last excluded this resource, for the backoffice's exclusion list. */\n excludedBy?: string;\n /** When it was last excluded, ISO-8601. */\n excludedAt?: string;\n}\n\nexport interface ResourceListResponse {\n items: ResourceSummary[];\n}\n\n/**\n * The shape of a resource's body (CONTEXT.md: bodies are type-shaped, not\n * uniformly markdown). Drives detail-panel rendering: markdown is rendered,\n * json is pretty-printed and syntax-highlighted (ADR-0014). `plugin` and\n * `marketplace` bodies are the real Claude Code `plugin.json` /\n * `marketplace.json` manifest — not a hand-authored pointer doc, superseding\n * ADR-0010's markdown-only body for `marketplace`.\n */\nexport type BodyShape = 'markdown' | 'json';\n\nexport function getBodyShape(type: ResourceType): BodyShape {\n return type === 'mcp-config' || type === 'plugin' || type === 'marketplace'\n ? 'json'\n : 'markdown';\n}\n\n/**\n * Whether the detail panel shows a \"Content\" section for this type.\n *\n * Suppressed for exactly the JSON-bodied types (`getBodyShape` === 'json'):\n * `plugin` and `marketplace` are containers whose value is the relationship\n * graph rather than the manifest, and an `mcp-config`'s body is a settings\n * fragment to merge — in all three the raw JSON buries the panel and is\n * better reached through Copy, Download or the install dialog, which is\n * where it is actionable. The markdown types keep their rendered body.\n */\nexport function showsContentSection(type: ResourceType): boolean {\n return getBodyShape(type) !== 'json';\n}\n\n/**\n * Whether downloading the body delivers the artifact itself. True for every\n * type: `plugin`/`marketplace` bodies are now the real manifest JSON, so a\n * download delivers that file itself rather than a non-actionable pointer\n * doc (ADR-0014, superseding ADR-0009's amendment and ADR-0010 on this\n * point).\n */\nexport function hasDownloadableArtifact(_type: ResourceType): boolean {\n return true;\n}\n\n/**\n * Whether copying the body to the clipboard is a meaningful install action.\n * True for every type: `marketplace`'s body is now the real manifest JSON,\n * not an instructions doc, so copying it is no longer a misrepresentation\n * (ADR-0014, superseding ADR-0010 on this point).\n */\nexport function hasCopyableBody(_type: ResourceType): boolean {\n return true;\n}\n\n/**\n * How a body reaches its install path.\n *\n * `drop-in` — the body becomes the file (or directory) at `path`, which is\n * the resource's own; writing it touches nothing else.\n *\n * `merge` — `path` is a shared settings file the user already owns, and the\n * body is a fragment to merge into it. The distinction is not cosmetic:\n * following a `merge` target as if it were a `drop-in` overwrites the user's\n * existing configuration, so the UI must never present the two identically.\n */\nexport type InstallMode = 'drop-in' | 'merge';\n\nexport interface ResourceInstallTarget {\n /** Workspace-relative path. */\n path: string;\n mode: InstallMode;\n}\n\n/**\n * The concrete framework tokens a body can be installed into — `all` is a\n * wildcard over these, never a destination itself.\n */\nexport const INSTALLABLE_FRAMEWORKS = [\n 'claude-code',\n 'github-copilot',\n 'google-gemini',\n 'cursor',\n 'opencode',\n] as const;\n\n/**\n * Convention table: (type, framework) → where the body goes and how.\n * A missing entry means the pair has no filesystem convention — `plugin` and\n * `marketplace` bodies are pointers that install through their framework\n * (ADR-0009, ADR-0010), so they carry install commands rather than paths.\n */\nconst INSTALL_PATHS: Record<\n ResourceType,\n Record<string, { mode: InstallMode; path: (name: string) => string }>\n> = {\n // Every host reads a skill as a self-contained directory. Copilot also\n // picks up `.claude/skills`, but each tool's own path is what we recommend;\n // `.agents/skills` is the cross-tool alias Copilot and Gemini both honour,\n // which makes it the right answer for an unrecognised framework.\n skill: {\n 'claude-code': { mode: 'drop-in', path: name => `.claude/skills/${name}/` },\n 'github-copilot': {\n mode: 'drop-in',\n path: name => `.github/skills/${name}/`,\n },\n 'google-gemini': {\n mode: 'drop-in',\n path: name => `.gemini/skills/${name}/`,\n },\n cursor: { mode: 'drop-in', path: name => `.cursor/skills/${name}/` },\n opencode: { mode: 'drop-in', path: name => `.opencode/skills/${name}/` },\n default: { mode: 'drop-in', path: name => `.agents/skills/${name}/` },\n },\n agent: {\n 'claude-code': {\n mode: 'drop-in',\n path: name => `.claude/agents/${name}.md`,\n },\n 'github-copilot': {\n mode: 'drop-in',\n path: name => `.github/agents/${name}.agent.md`,\n },\n 'google-gemini': {\n mode: 'drop-in',\n path: name => `.gemini/agents/${name}.md`,\n },\n cursor: { mode: 'drop-in', path: name => `.cursor/rules/${name}.mdc` },\n opencode: { mode: 'drop-in', path: name => `.opencode/agents/${name}.md` },\n default: { mode: 'drop-in', path: name => `.ai/agents/${name}.md` },\n },\n // All four hosts have a hook system, but only Copilot gives each hook its\n // own file; the rest register hooks inside a shared settings document.\n // OpenCode has no declarative hook config at all — hooks are exclusively\n // JS/TS plugin modules — so it deliberately carries no entry here, the same\n // way it carries no `default`.\n hook: {\n 'claude-code': { mode: 'merge', path: () => `.claude/settings.json` },\n 'github-copilot': {\n mode: 'drop-in',\n path: name => `.github/hooks/${name}.json`,\n },\n 'google-gemini': { mode: 'merge', path: () => `.gemini/settings.json` },\n cursor: { mode: 'merge', path: () => `.cursor/hooks.json` },\n },\n 'mcp-config': {\n 'claude-code': { mode: 'merge', path: () => `.mcp.json` },\n 'github-copilot': { mode: 'merge', path: () => `.vscode/mcp.json` },\n 'google-gemini': { mode: 'merge', path: () => `.gemini/settings.json` },\n cursor: { mode: 'merge', path: () => `.cursor/mcp.json` },\n opencode: { mode: 'merge', path: () => `opencode.json` },\n },\n plugin: {},\n marketplace: {},\n};\n\n/**\n * Where a resource's body installs for one framework, and whether that path\n * is the body's own file or a settings file to merge into. Returns\n * `undefined` when the (type, framework) pair has no filesystem convention.\n */\nexport function getResourceInstallTarget(\n type: ResourceType,\n framework: string,\n name: string,\n): ResourceInstallTarget | undefined {\n const conventions = INSTALL_PATHS[type];\n const entry =\n conventions[normalizeFramework(framework)] ?? conventions.default;\n return entry ? { path: entry.path(name), mode: entry.mode } : undefined;\n}\n\n/**\n * The frameworks to show install paths for. An empty list means the resource\n * declared no compatibility and gets the neutral `default` convention; `all`\n * is a wildcard and expands to every installable host, so it never collapses\n * to a single row — or, for types with no `default`, to no rows at all.\n */\nexport function expandInstallFrameworks(frameworks: string[]): string[] {\n const normalized = dedupe(frameworks.map(normalizeFramework));\n if (normalized.length === 0) {\n return ['default'];\n }\n if (normalized.includes('all')) {\n return [...INSTALLABLE_FRAMEWORKS];\n }\n return normalized;\n}\n\n/*\n * Marketplace journey helpers (ADR-0010). A marketplace installs in two\n * steps — register the catalog with the AI tool, then install plugins from\n * it. Both Claude Code and Copilot CLI read the same\n * `.claude-plugin/marketplace.json` format and address the repo as\n * `owner/repo`; the plugin derives that slug from the mandatory\n * `source-location` (the producer contract requires the body doc to live\n * inside the marketplace repo). No derivation → the UI falls back to the\n * rendered body, which stays canonical.\n */\n\n/** Frameworks with a marketplace-add concept (Cursor/Gemini have none). */\nexport const MARKETPLACE_CAPABLE_FRAMEWORKS = [\n 'claude-code',\n 'github-copilot',\n] as const;\n\n/**\n * Narrow a resource's declared frameworks to those a given journey can\n * actually launch. A deep link is a claim about compatibility: offering\n * \"Install in Claude\" for a resource that never declared `claude-code` tells\n * the user something untrue about the resource.\n *\n * `all` and an empty list both mean \"unrestricted\" and expand to every\n * capable host. Anything else is intersected, so a declared framework with no\n * handler (Gemini) — or an unknown token — yields no links rather than\n * silently widening to every host.\n */\nexport function resolveCapableFrameworks(\n declared: string[],\n capable: readonly string[],\n): string[] {\n const normalized = dedupe(declared.map(normalizeFramework));\n if (normalized.length === 0 || normalized.includes('all')) {\n return [...capable];\n }\n return normalized.filter(f => capable.includes(f));\n}\n\n/**\n * Derive the `owner/repo` slug from a marketplace's `source-location`.\n * GitHub URLs only — other hosts return `undefined` (body-only fallback).\n */\nexport function getMarketplaceRepoSlug(\n sourceLocation: string | undefined,\n): string | undefined {\n if (!sourceLocation) {\n return undefined;\n }\n let url: URL;\n try {\n url = new URL(sourceLocation.replace(/^url:/, ''));\n } catch {\n return undefined;\n }\n if (url.hostname !== 'github.com') {\n return undefined;\n }\n const [owner, repo] = url.pathname.split('/').filter(Boolean);\n if (!owner || !repo) {\n return undefined;\n }\n return `${owner}/${repo.replace(/\\.git$/, '')}`;\n}\n\n/**\n * A one-click launcher for an add command: a custom-scheme URL that opens\n * the host app with the command pre-filled in the prompt box. The host\n * never auto-executes it — the user reviews and presses Enter — and an\n * unregistered scheme is a harmless no-op, so these complement the copy\n * button rather than replace it.\n */\nexport interface ResourceDeepLink {\n /** Host application name; render as \"Add in <label>\" / \"Install in <label>\". */\n label: string;\n href: string;\n /**\n * The framework token this launcher installs into, for the `tool` column on\n * the `install` event it records (ADR-0011). Not 1:1 with `label`:\n * `github-copilot` offers both \"VS Code\" and \"VS Code Insiders\".\n *\n * Optional only to keep this published interface additive — every link the\n * generators below build sets it, which `resources.test.ts` pins. A caller\n * reading a generated link can rely on it; one constructing its own shape\n * is not forced to supply it.\n */\n framework?: string;\n}\n\nexport interface MarketplaceAddCommand {\n framework: string;\n command: string;\n deepLinks: ResourceDeepLink[];\n}\n\n/**\n * The copyable marketplace-add command per compatible framework. `all`,\n * unknown-only, or empty framework lists expand to every capable framework;\n * frameworks without a marketplace concept produce no row.\n *\n * Claude Code carries a single deep link on the CLI's documented\n * `claude-cli://open?q=` handler, which opens a Claude Code terminal\n * session with the command pre-filled regardless of the user's editor.\n *\n * Copilot's CLI still has no URI scheme, but Copilot's *other* surface does:\n * VS Code 1.113 ships `vscode://chat-plugin/add-marketplace?ref=`, which\n * accepts a plain or base64 `owner/repo` and always shows a confirmation\n * dialog before registering the marketplace (it also dedupes against the\n * user's existing entries). So the Copilot row keeps its terminal command for\n * CLI users *and* gains editor launchers, mirroring how `mcp-config` already\n * maps `github-copilot` onto the VS Code stable/Insiders pair.\n */\nexport function getMarketplaceAddCommands(\n frameworks: string[],\n repoSlug: string,\n): MarketplaceAddCommand[] {\n const capable = resolveCapableFrameworks(\n frameworks,\n MARKETPLACE_CAPABLE_FRAMEWORKS,\n );\n const commands: MarketplaceAddCommand[] = [];\n for (const framework of capable) {\n if (framework === 'claude-code') {\n const command = `/plugin marketplace add ${repoSlug}`;\n commands.push({\n framework,\n command,\n deepLinks: [\n {\n label: 'Claude',\n href: `claude-cli://open?q=${encodeURIComponent(command)}`,\n framework,\n },\n ],\n });\n } else if (framework === 'github-copilot') {\n const ref = encodeURIComponent(repoSlug);\n commands.push({\n framework,\n command: `copilot plugin marketplace add ${repoSlug}`,\n deepLinks: [\n {\n label: 'VS Code',\n href: `vscode://chat-plugin/add-marketplace?ref=${ref}`,\n framework,\n },\n {\n label: 'VS Code Insiders',\n href: `vscode-insiders://chat-plugin/add-marketplace?ref=${ref}`,\n framework,\n },\n ],\n });\n }\n }\n return commands;\n}\n\n/** The raw-file URL for a GitHub blob `source-location`; else `undefined`. */\nfunction getRawGithubFileUrl(\n sourceLocation: string | undefined,\n): string | undefined {\n if (!sourceLocation) {\n return undefined;\n }\n let url: URL;\n try {\n url = new URL(sourceLocation.replace(/^url:/, ''));\n } catch {\n return undefined;\n }\n if (url.hostname !== 'github.com') {\n return undefined;\n }\n const [owner, repo, marker, ref, ...path] = url.pathname\n .split('/')\n .filter(Boolean);\n if (!owner || !repo || marker !== 'blob' || !ref || path.length === 0) {\n return undefined;\n }\n return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path.join(\n '/',\n )}`;\n}\n\n/**\n * A launcher that opens an agent with an install prompt pre-filled. Neither\n * host auto-executes: the user reads the prompt and presses Enter. Claude Code\n * and Cursor are the two tools exposing a generic prompt URI — Copilot's\n * editor has purpose-built routes but no prompt handler, and Gemini has\n * neither (its `gemini.google.com/app?prompt=` link drives the web app, which\n * cannot write to a local workspace).\n *\n * Cursor truncates a deeplink at the first raw `&`, so the prompt must be\n * percent-encoded — `encodeURIComponent` handles that. Cursor also caps the\n * URL at ~8000 characters; these prompts are a URL plus a path, far under it.\n */\nfunction claudePromptLink(prompt: string): ResourceDeepLink {\n return {\n label: 'Claude',\n href: `claude-cli://open?q=${encodeURIComponent(prompt)}`,\n framework: 'claude-code',\n };\n}\n\nfunction cursorPromptLink(prompt: string): ResourceDeepLink {\n return {\n label: 'Cursor',\n href: `cursor://anysphere.cursor-deeplink/prompt?text=${encodeURIComponent(\n prompt,\n )}`,\n framework: 'cursor',\n };\n}\n\n/**\n * Frameworks with a one-click agent-install handler (Gemini has none).\n * OpenCode's Desktop app does register a real `opencode://` scheme, but every\n * route (`open-project`, `new-session`) requires an absolute local\n * `directory` param a web page cannot supply, so it's excluded here too.\n */\nexport const AGENT_LINK_CAPABLE_FRAMEWORKS = [\n 'claude-code',\n 'github-copilot',\n 'cursor',\n] as const;\n\n/** Hosts reachable by a generic prompt URI — the only route for skill/hook. */\nexport const PROMPT_LINK_CAPABLE_FRAMEWORKS = [\n 'claude-code',\n 'cursor',\n] as const;\n\n/**\n * The plain http(s) URL behind a `source-location`. Unlike\n * `getRawGithubFileUrl` this accepts **directory (`tree`) URLs**, which is what\n * a multi-file `skill` actually carries — a raw-file URL would resolve to\n * `undefined` for exactly the type that most needs a launcher.\n */\nfunction getSourceUrl(sourceLocation: string | undefined): string | undefined {\n if (!sourceLocation) {\n return undefined;\n }\n try {\n const url = new URL(sourceLocation.replace(/^url:/, ''));\n return url.protocol === 'http:' || url.protocol === 'https:'\n ? url.toString()\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * One-click launchers for the types with no purpose-built install route —\n * `skill` and `hook`. Both ride the generic prompt handlers, so the prompt\n * names the *host's own* install path and respects its install mode: a `merge`\n * target is asked to be merged into, never overwritten, so a launcher can\n * never cost the user their existing settings.\n *\n * Returns `[]` when the source URL cannot be derived, or for hosts with no\n * prompt route (Copilot, Gemini) — the path list and copy/download remain.\n */\nexport function getPromptInstallLinks(\n type: ResourceType,\n sourceLocation: string | undefined,\n name: string,\n frameworks: string[] = [],\n): ResourceDeepLink[] {\n return getInstallSteps(type, sourceLocation, name, frameworks)\n .map(step => step.link)\n .filter((link): link is ResourceDeepLink => !!link);\n}\n\n/**\n * Everything a single host needs to install a resource: where the body goes,\n * the prompt that puts it there, and a launcher when the host has a URI to\n * launch.\n *\n * `link` is absent for hosts with no prompt route — Copilot and Gemini. That\n * absence is why the step carries `prompt` as text: a host without a URI is\n * not a host without an install path, and the same instruction that a launcher\n * would pre-fill can be pasted into that agent by hand. Every declared\n * framework therefore gets something actionable, not a bare path.\n */\nexport interface ResourceInstallStep {\n framework: string;\n target: ResourceInstallTarget;\n /** The install instruction, ready to run in that host's agent. */\n prompt: string;\n /** One-click launcher, where the host exposes a generic prompt URI. */\n link?: ResourceDeepLink;\n}\n\nexport function getInstallSteps(\n type: ResourceType,\n sourceLocation: string | undefined,\n name: string,\n frameworks: string[] = [],\n): ResourceInstallStep[] {\n const sourceUrl = getSourceUrl(sourceLocation);\n if (!sourceUrl) {\n return [];\n }\n const label = RESOURCE_TYPE_REGISTRY[type].label.toLowerCase();\n const steps: ResourceInstallStep[] = [];\n for (const framework of expandInstallFrameworks(frameworks)) {\n const target = getResourceInstallTarget(type, framework, name);\n if (!target) {\n continue;\n }\n const prompt =\n target.mode === 'merge'\n ? `Install the \"${name}\" ${label}: fetch ${sourceUrl} and merge it into ${target.path}, keeping my existing settings intact.`\n : `Install the \"${name}\" ${label}: fetch ${sourceUrl} and save it to ${target.path}.`;\n let link: ResourceDeepLink | undefined;\n if (framework === 'claude-code') {\n link = claudePromptLink(prompt);\n } else if (framework === 'cursor') {\n link = cursorPromptLink(prompt);\n }\n steps.push({ framework, target, prompt, link });\n }\n return steps;\n}\n\n/** Frameworks with a one-click MCP-install handler (Gemini has none). */\nexport const MCP_LINK_CAPABLE_FRAMEWORKS = [\n 'claude-code',\n 'github-copilot',\n 'cursor',\n] as const;\n\n/**\n * One-click install links for an `agent` resource, restricted to the\n * resource's own compatible frameworks. VS Code stable and Insiders use the\n * native `chat-agent/install?url=` handler (the mechanism behind\n * awesome-copilot's Install buttons) and are Copilot's hosts, so they follow\n * `github-copilot`: VS Code downloads the file at `url` and asks the user\n * where to save it. Claude Code uses the `claude-cli://open?q=` handler with\n * an install prompt pre-filled to the convention path — reviewed and sent by\n * the user, never auto-executed. All derive from the GitHub blob URL in\n * `source-location`; anything else returns `[]` (copy/download fallback).\n */\nexport function getAgentInstallLinks(\n sourceLocation: string | undefined,\n name: string,\n frameworks: string[] = [],\n): ResourceDeepLink[] {\n const rawUrl = getRawGithubFileUrl(sourceLocation);\n if (!rawUrl) {\n return [];\n }\n const capable = resolveCapableFrameworks(\n frameworks,\n AGENT_LINK_CAPABLE_FRAMEWORKS,\n );\n const encodedRawUrl = encodeURIComponent(rawUrl);\n const installPrompt = (framework: string) => {\n const target = getResourceInstallTarget('agent', framework, name);\n return `Install this agent: fetch ${rawUrl} and save it to ${target?.path}`;\n };\n const links: ResourceDeepLink[] = [];\n for (const framework of capable) {\n if (framework === 'claude-code') {\n links.push(claudePromptLink(installPrompt('claude-code')));\n } else if (framework === 'cursor') {\n links.push(cursorPromptLink(installPrompt('cursor')));\n } else if (framework === 'github-copilot') {\n links.push({\n label: 'VS Code',\n href: `vscode:chat-agent/install?url=${encodedRawUrl}`,\n framework,\n });\n links.push({\n label: 'VS Code Insiders',\n href: `vscode-insiders:chat-agent/install?url=${encodedRawUrl}`,\n framework,\n });\n }\n }\n return links;\n}\n\n/**\n * One-click install links for an `mcp-config` resource, derived from its body —\n * the canonical `.mcp.json` snippet (spec §3.4). VS Code carries a native\n * `vscode:mcp/install?{json}` handler (Insiders scheme twin), Cursor a\n * documented `cursor://anysphere.cursor-deeplink/mcp/install` one, and\n * Claude Code gets a `claude-cli://open?q=` prompt around\n * `claude mcp add-json` — pre-filled, reviewed, never auto-executed.\n * Hosts follow the resource's own compatible frameworks: `all`/empty expand\n * to every capable host, anything else is intersected, so an mcp-config\n * declaring only Gemini (no handler) gets no links rather than every host's.\n * A missing, unparseable, or multi-server body returns `[]` (copy fallback) —\n * the body stays canonical, links are conveniences derived from it.\n */\nexport function getMcpInstallLinks(\n frameworks: string[],\n resourceName: string,\n bodyContent: string | undefined,\n): ResourceDeepLink[] {\n if (!bodyContent) {\n return [];\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(bodyContent);\n } catch {\n return [];\n }\n if (typeof parsed !== 'object' || parsed === null) {\n return [];\n }\n\n let serverName = resourceName;\n let config: Record<string, unknown>;\n const servers = (parsed as { mcpServers?: unknown }).mcpServers;\n if (servers !== undefined) {\n if (typeof servers !== 'object' || servers === null) {\n return [];\n }\n const entries = Object.entries(servers as Record<string, unknown>);\n if (\n entries.length !== 1 ||\n typeof entries[0][1] !== 'object' ||\n entries[0][1] === null\n ) {\n return [];\n }\n serverName = entries[0][0];\n config = entries[0][1] as Record<string, unknown>;\n } else if ('command' in parsed || 'url' in parsed) {\n config = parsed as Record<string, unknown>;\n } else {\n return [];\n }\n\n const capable = resolveCapableFrameworks(\n frameworks,\n MCP_LINK_CAPABLE_FRAMEWORKS,\n );\n\n const configJson = JSON.stringify(config);\n const links: ResourceDeepLink[] = [];\n for (const framework of capable) {\n if (framework === 'claude-code') {\n const prompt = encodeURIComponent(\n `Install this MCP config by running: claude mcp add-json ${serverName} '${configJson}'`,\n );\n links.push({\n label: 'Claude',\n href: `claude-cli://open?q=${prompt}`,\n framework,\n });\n } else if (framework === 'github-copilot') {\n const vsConfig = encodeURIComponent(\n JSON.stringify({ name: serverName, ...config }),\n );\n links.push({\n label: 'VS Code',\n href: `vscode:mcp/install?${vsConfig}`,\n framework,\n });\n links.push({\n label: 'VS Code Insiders',\n href: `vscode-insiders:mcp/install?${vsConfig}`,\n framework,\n });\n } else if (framework === 'cursor') {\n try {\n links.push({\n label: 'Cursor',\n href: `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(\n serverName,\n )}&config=${btoa(configJson)}`,\n framework,\n });\n } catch {\n // btoa rejects non-Latin1 config content — skip the Cursor link.\n }\n }\n }\n return links;\n}\n\n/**\n * Step two of the journey: how to install a plugin from the marketplace.\n * `marketplaceName` is the entity name, which the producer contract requires\n * to equal the `name` in `marketplace.json`.\n */\nexport function getMarketplaceInstallTemplate(marketplaceName: string): string {\n return `/plugin install <plugin>@${marketplaceName}`;\n}\n\n/**\n * The `.claude/settings.json` snippet a team commits so collaborators are\n * prompted to install the marketplace automatically (Claude Code).\n */\nexport function getMarketplaceTeamSnippet(\n marketplaceName: string,\n repoSlug: string,\n): string {\n return JSON.stringify(\n {\n extraKnownMarketplaces: {\n [marketplaceName]: { source: { source: 'github', repo: repoSlug } },\n },\n },\n null,\n 2,\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+
5
+ const SortModeEnum = zod.z.enum(["usage", "newest", "name"]);
6
+ const SORT_MODES = SortModeEnum.options;
7
+ function isSortMode(value) {
8
+ return typeof value === "string" && SORT_MODES.includes(value);
9
+ }
10
+ const DEFAULT_SORT_MODE = "usage";
11
+ const NATURAL_SORT_DIRECTION = {
12
+ usage: "desc",
13
+ newest: "desc",
14
+ name: "asc"
15
+ };
16
+ const VIEW_WEIGHT = 0.8;
17
+ function usageScore(counts) {
18
+ if (!counts) return 0;
19
+ return (counts.install ?? 0) + (counts.view ?? 0) * VIEW_WEIGHT;
20
+ }
21
+ function firstSeenKey(r) {
22
+ return r.firstSeenAt ? Date.parse(r.firstSeenAt) : -Infinity;
23
+ }
24
+ function nameKey(r) {
25
+ return (r.title ?? r.name).toLowerCase();
26
+ }
27
+ function compareNameKey(a, b) {
28
+ return nameKey(a).localeCompare(nameKey(b));
29
+ }
30
+ function compareNames(a, b) {
31
+ const byName = compareNameKey(a, b);
32
+ if (byName !== 0) return byName;
33
+ return a.entityRef.localeCompare(b.entityRef);
34
+ }
35
+ function primaryCompare(mode, counts) {
36
+ switch (mode) {
37
+ case "usage":
38
+ return (a, b) => usageScore(counts.get(a.entityRef)) - usageScore(counts.get(b.entityRef));
39
+ case "newest":
40
+ return (a, b) => {
41
+ const ka = firstSeenKey(a);
42
+ const kb = firstSeenKey(b);
43
+ if (ka === kb) return 0;
44
+ return ka < kb ? -1 : 1;
45
+ };
46
+ case "name":
47
+ return compareNameKey;
48
+ default: {
49
+ const exhaustiveCheck = mode;
50
+ throw new Error(`Unhandled sort mode: ${exhaustiveCheck}`);
51
+ }
52
+ }
53
+ }
54
+ function sortResources(resources, mode, direction, counts) {
55
+ const compare = primaryCompare(mode, counts);
56
+ const sign = direction === "asc" ? 1 : -1;
57
+ return [...resources].sort((a, b) => {
58
+ const primary = compare(a, b);
59
+ if (primary !== 0) return sign * primary;
60
+ return compareNames(a, b);
61
+ });
62
+ }
63
+
64
+ exports.DEFAULT_SORT_MODE = DEFAULT_SORT_MODE;
65
+ exports.NATURAL_SORT_DIRECTION = NATURAL_SORT_DIRECTION;
66
+ exports.SORT_MODES = SORT_MODES;
67
+ exports.SortModeEnum = SortModeEnum;
68
+ exports.isSortMode = isSortMode;
69
+ exports.sortResources = sortResources;
70
+ exports.usageScore = usageScore;
71
+ //# sourceMappingURL=sorting.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sorting.cjs.js","sources":["../src/sorting.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { ResourceSummary } from './resources';\nimport type { TelemetryCounts } from './telemetry';\n\n/**\n * The named sort modes exposed on the browse page (issue #102). `usage` is\n * the composite \"most popular\" score; `newest` orders by when the plugin\n * first saw each resource (ADR-0021); `name` is the alphabetical fallback\n * every list needs.\n *\n * The raw per-counter modes (installs, downloads, views) and \"Recommended\n * first\" were dropped: `usage` already folds the counters into one ranking,\n * and recommended resources are pinned in their own section above the grid,\n * so sorting by the flag would only reorder a list it no longer contains.\n *\n * Deliberately no `framework` mode: `frameworks` is a multi-value array per\n * resource, so a resource compatible with several tools has no single sort\n * position — that dimension is a filter (the AI Tool dropdown), not a sort.\n */\nexport const SortModeEnum = z.enum(['usage', 'newest', 'name']);\nexport type SortMode = z.infer<typeof SortModeEnum>;\n\nexport const SORT_MODES: readonly SortMode[] = SortModeEnum.options;\n\nexport function isSortMode(value: unknown): value is SortMode {\n return (\n typeof value === 'string' &&\n (SORT_MODES as readonly string[]).includes(value)\n );\n}\n\nexport type SortDirection = 'asc' | 'desc';\n\n/**\n * The default sort mode when no configuration is set and the mode a deployer\n * gets on an unrecognised `devAiHub.ui.defaultSort` value (soft fallback —\n * a typo'd config value must never blank the page).\n */\nexport const DEFAULT_SORT_MODE: SortMode = 'usage';\n\n/**\n * Each mode's \"obvious\" direction — usage and newest rank highest (most\n * popular, most recent) first, name reads A-Z. The dropdown and direction toggle reset to this whenever the user\n * switches mode.\n */\nexport const NATURAL_SORT_DIRECTION: Record<SortMode, SortDirection> = {\n usage: 'desc',\n newest: 'desc',\n name: 'asc',\n};\n\n/**\n * The composite \"most popular\" score (issue #102 default): installs count\n * for more than views, but a heavily-viewed, rarely-installed resource still\n * earns some credit. Weights are fixed in code rather than configurable —\n * see the design discussion — so \"usage\" ranks the same in every deployment.\n */\nconst VIEW_WEIGHT = 0.8;\n\nexport function usageScore(counts: TelemetryCounts | undefined): number {\n if (!counts) return 0;\n return (counts.install ?? 0) + (counts.view ?? 0) * VIEW_WEIGHT;\n}\n\n/**\n * A resource that predates first-seen tracking has no date and ranks as the\n * oldest — last when newest-first, first when oldest-first.\n */\nfunction firstSeenKey(r: ResourceSummary): number {\n return r.firstSeenAt ? Date.parse(r.firstSeenAt) : -Infinity;\n}\n\nfunction nameKey(r: ResourceSummary): string {\n return (r.title ?? r.name).toLowerCase();\n}\n\nfunction compareNameKey(a: ResourceSummary, b: ResourceSummary): number {\n return nameKey(a).localeCompare(nameKey(b));\n}\n\n/** Ascending string compare, used for the Name mode and as the universal tie-breaker. */\nfunction compareNames(a: ResourceSummary, b: ResourceSummary): number {\n const byName = compareNameKey(a, b);\n if (byName !== 0) return byName;\n // Identical titles: fall back to entityRef so ordering never depends on\n // input order for two same-named resources.\n return a.entityRef.localeCompare(b.entityRef);\n}\n\nfunction primaryCompare(\n mode: SortMode,\n counts: Map<string, TelemetryCounts>,\n): (a: ResourceSummary, b: ResourceSummary) => number {\n switch (mode) {\n case 'usage':\n return (a, b) =>\n usageScore(counts.get(a.entityRef)) -\n usageScore(counts.get(b.entityRef));\n case 'newest':\n return (a, b) => {\n // Compared rather than subtracted: two undated resources are\n // -Infinity each, and -Infinity - -Infinity is NaN.\n const ka = firstSeenKey(a);\n const kb = firstSeenKey(b);\n if (ka === kb) return 0;\n return ka < kb ? -1 : 1;\n };\n case 'name':\n // Name mode should only apply direction to the display-name key;\n // ties still fall back to entityRef in ascending order via\n // the shared compareNames tie-breaker below.\n return compareNameKey;\n default: {\n // Exhaustiveness guard: `SortMode` is a closed union, so every real\n // case is handled above — this only fires if that union grows\n // without a matching case here.\n const exhaustiveCheck: never = mode;\n throw new Error(`Unhandled sort mode: ${exhaustiveCheck}`);\n }\n }\n}\n\n/**\n * Sort resources by the given mode and direction. `counts` is the shared\n * page-level telemetry summary map (entityRef -> raw counts) — a resource\n * absent from the map is treated as all-zero, per `TelemetrySummary`'s\n * contract.\n *\n * Every mode tie-breaks on Name (A-Z) so equal-scoring resources — the\n * common case for a catalog with little or no telemetry yet — render in a\n * predictable, stable order rather than arbitrary fetch order. The `name`\n * mode itself has no primary key to break ties on, so it tie-breaks on\n * `entityRef` instead (see `compareNames`).\n *\n * Pure and allocation-light: does not mutate `resources`.\n */\nexport function sortResources(\n resources: readonly ResourceSummary[],\n mode: SortMode,\n direction: SortDirection,\n counts: Map<string, TelemetryCounts>,\n): ResourceSummary[] {\n const compare = primaryCompare(mode, counts);\n const sign = direction === 'asc' ? 1 : -1;\n\n return [...resources].sort((a, b) => {\n const primary = compare(a, b);\n if (primary !== 0) return sign * primary;\n return compareNames(a, b);\n });\n}\n"],"names":["z"],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod';
2
+
3
+ const SortModeEnum = z.enum(["usage", "newest", "name"]);
4
+ const SORT_MODES = SortModeEnum.options;
5
+ function isSortMode(value) {
6
+ return typeof value === "string" && SORT_MODES.includes(value);
7
+ }
8
+ const DEFAULT_SORT_MODE = "usage";
9
+ const NATURAL_SORT_DIRECTION = {
10
+ usage: "desc",
11
+ newest: "desc",
12
+ name: "asc"
13
+ };
14
+ const VIEW_WEIGHT = 0.8;
15
+ function usageScore(counts) {
16
+ if (!counts) return 0;
17
+ return (counts.install ?? 0) + (counts.view ?? 0) * VIEW_WEIGHT;
18
+ }
19
+ function firstSeenKey(r) {
20
+ return r.firstSeenAt ? Date.parse(r.firstSeenAt) : -Infinity;
21
+ }
22
+ function nameKey(r) {
23
+ return (r.title ?? r.name).toLowerCase();
24
+ }
25
+ function compareNameKey(a, b) {
26
+ return nameKey(a).localeCompare(nameKey(b));
27
+ }
28
+ function compareNames(a, b) {
29
+ const byName = compareNameKey(a, b);
30
+ if (byName !== 0) return byName;
31
+ return a.entityRef.localeCompare(b.entityRef);
32
+ }
33
+ function primaryCompare(mode, counts) {
34
+ switch (mode) {
35
+ case "usage":
36
+ return (a, b) => usageScore(counts.get(a.entityRef)) - usageScore(counts.get(b.entityRef));
37
+ case "newest":
38
+ return (a, b) => {
39
+ const ka = firstSeenKey(a);
40
+ const kb = firstSeenKey(b);
41
+ if (ka === kb) return 0;
42
+ return ka < kb ? -1 : 1;
43
+ };
44
+ case "name":
45
+ return compareNameKey;
46
+ default: {
47
+ const exhaustiveCheck = mode;
48
+ throw new Error(`Unhandled sort mode: ${exhaustiveCheck}`);
49
+ }
50
+ }
51
+ }
52
+ function sortResources(resources, mode, direction, counts) {
53
+ const compare = primaryCompare(mode, counts);
54
+ const sign = direction === "asc" ? 1 : -1;
55
+ return [...resources].sort((a, b) => {
56
+ const primary = compare(a, b);
57
+ if (primary !== 0) return sign * primary;
58
+ return compareNames(a, b);
59
+ });
60
+ }
61
+
62
+ export { DEFAULT_SORT_MODE, NATURAL_SORT_DIRECTION, SORT_MODES, SortModeEnum, isSortMode, sortResources, usageScore };
63
+ //# sourceMappingURL=sorting.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sorting.esm.js","sources":["../src/sorting.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { ResourceSummary } from './resources';\nimport type { TelemetryCounts } from './telemetry';\n\n/**\n * The named sort modes exposed on the browse page (issue #102). `usage` is\n * the composite \"most popular\" score; `newest` orders by when the plugin\n * first saw each resource (ADR-0021); `name` is the alphabetical fallback\n * every list needs.\n *\n * The raw per-counter modes (installs, downloads, views) and \"Recommended\n * first\" were dropped: `usage` already folds the counters into one ranking,\n * and recommended resources are pinned in their own section above the grid,\n * so sorting by the flag would only reorder a list it no longer contains.\n *\n * Deliberately no `framework` mode: `frameworks` is a multi-value array per\n * resource, so a resource compatible with several tools has no single sort\n * position — that dimension is a filter (the AI Tool dropdown), not a sort.\n */\nexport const SortModeEnum = z.enum(['usage', 'newest', 'name']);\nexport type SortMode = z.infer<typeof SortModeEnum>;\n\nexport const SORT_MODES: readonly SortMode[] = SortModeEnum.options;\n\nexport function isSortMode(value: unknown): value is SortMode {\n return (\n typeof value === 'string' &&\n (SORT_MODES as readonly string[]).includes(value)\n );\n}\n\nexport type SortDirection = 'asc' | 'desc';\n\n/**\n * The default sort mode when no configuration is set and the mode a deployer\n * gets on an unrecognised `devAiHub.ui.defaultSort` value (soft fallback —\n * a typo'd config value must never blank the page).\n */\nexport const DEFAULT_SORT_MODE: SortMode = 'usage';\n\n/**\n * Each mode's \"obvious\" direction — usage and newest rank highest (most\n * popular, most recent) first, name reads A-Z. The dropdown and direction toggle reset to this whenever the user\n * switches mode.\n */\nexport const NATURAL_SORT_DIRECTION: Record<SortMode, SortDirection> = {\n usage: 'desc',\n newest: 'desc',\n name: 'asc',\n};\n\n/**\n * The composite \"most popular\" score (issue #102 default): installs count\n * for more than views, but a heavily-viewed, rarely-installed resource still\n * earns some credit. Weights are fixed in code rather than configurable —\n * see the design discussion — so \"usage\" ranks the same in every deployment.\n */\nconst VIEW_WEIGHT = 0.8;\n\nexport function usageScore(counts: TelemetryCounts | undefined): number {\n if (!counts) return 0;\n return (counts.install ?? 0) + (counts.view ?? 0) * VIEW_WEIGHT;\n}\n\n/**\n * A resource that predates first-seen tracking has no date and ranks as the\n * oldest — last when newest-first, first when oldest-first.\n */\nfunction firstSeenKey(r: ResourceSummary): number {\n return r.firstSeenAt ? Date.parse(r.firstSeenAt) : -Infinity;\n}\n\nfunction nameKey(r: ResourceSummary): string {\n return (r.title ?? r.name).toLowerCase();\n}\n\nfunction compareNameKey(a: ResourceSummary, b: ResourceSummary): number {\n return nameKey(a).localeCompare(nameKey(b));\n}\n\n/** Ascending string compare, used for the Name mode and as the universal tie-breaker. */\nfunction compareNames(a: ResourceSummary, b: ResourceSummary): number {\n const byName = compareNameKey(a, b);\n if (byName !== 0) return byName;\n // Identical titles: fall back to entityRef so ordering never depends on\n // input order for two same-named resources.\n return a.entityRef.localeCompare(b.entityRef);\n}\n\nfunction primaryCompare(\n mode: SortMode,\n counts: Map<string, TelemetryCounts>,\n): (a: ResourceSummary, b: ResourceSummary) => number {\n switch (mode) {\n case 'usage':\n return (a, b) =>\n usageScore(counts.get(a.entityRef)) -\n usageScore(counts.get(b.entityRef));\n case 'newest':\n return (a, b) => {\n // Compared rather than subtracted: two undated resources are\n // -Infinity each, and -Infinity - -Infinity is NaN.\n const ka = firstSeenKey(a);\n const kb = firstSeenKey(b);\n if (ka === kb) return 0;\n return ka < kb ? -1 : 1;\n };\n case 'name':\n // Name mode should only apply direction to the display-name key;\n // ties still fall back to entityRef in ascending order via\n // the shared compareNames tie-breaker below.\n return compareNameKey;\n default: {\n // Exhaustiveness guard: `SortMode` is a closed union, so every real\n // case is handled above — this only fires if that union grows\n // without a matching case here.\n const exhaustiveCheck: never = mode;\n throw new Error(`Unhandled sort mode: ${exhaustiveCheck}`);\n }\n }\n}\n\n/**\n * Sort resources by the given mode and direction. `counts` is the shared\n * page-level telemetry summary map (entityRef -> raw counts) — a resource\n * absent from the map is treated as all-zero, per `TelemetrySummary`'s\n * contract.\n *\n * Every mode tie-breaks on Name (A-Z) so equal-scoring resources — the\n * common case for a catalog with little or no telemetry yet — render in a\n * predictable, stable order rather than arbitrary fetch order. The `name`\n * mode itself has no primary key to break ties on, so it tie-breaks on\n * `entityRef` instead (see `compareNames`).\n *\n * Pure and allocation-light: does not mutate `resources`.\n */\nexport function sortResources(\n resources: readonly ResourceSummary[],\n mode: SortMode,\n direction: SortDirection,\n counts: Map<string, TelemetryCounts>,\n): ResourceSummary[] {\n const compare = primaryCompare(mode, counts);\n const sign = direction === 'asc' ? 1 : -1;\n\n return [...resources].sort((a, b) => {\n const primary = compare(a, b);\n if (primary !== 0) return sign * primary;\n return compareNames(a, b);\n });\n}\n"],"names":[],"mappings":";;AAmBO,MAAM,eAAe,CAAA,CAAE,IAAA,CAAK,CAAC,OAAA,EAAS,QAAA,EAAU,MAAM,CAAC;AAGvD,MAAM,aAAkC,YAAA,CAAa;AAErD,SAAS,WAAW,KAAA,EAAmC;AAC5D,EAAA,OACE,OAAO,KAAA,KAAU,QAAA,IAChB,UAAA,CAAiC,SAAS,KAAK,CAAA;AAEpD;AASO,MAAM,iBAAA,GAA8B;AAOpC,MAAM,sBAAA,GAA0D;AAAA,EACrE,KAAA,EAAO,MAAA;AAAA,EACP,MAAA,EAAQ,MAAA;AAAA,EACR,IAAA,EAAM;AACR;AAQA,MAAM,WAAA,GAAc,GAAA;AAEb,SAAS,WAAW,MAAA,EAA6C;AACtE,EAAA,IAAI,CAAC,QAAQ,OAAO,CAAA;AACpB,EAAA,OAAA,CAAQ,MAAA,CAAO,OAAA,IAAW,CAAA,IAAA,CAAM,MAAA,CAAO,QAAQ,CAAA,IAAK,WAAA;AACtD;AAMA,SAAS,aAAa,CAAA,EAA4B;AAChD,EAAA,OAAO,EAAE,WAAA,GAAc,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,WAAW,CAAA,GAAI,CAAA,QAAA;AACrD;AAEA,SAAS,QAAQ,CAAA,EAA4B;AAC3C,EAAA,OAAA,CAAQ,CAAA,CAAE,KAAA,IAAS,CAAA,CAAE,IAAA,EAAM,WAAA,EAAY;AACzC;AAEA,SAAS,cAAA,CAAe,GAAoB,CAAA,EAA4B;AACtE,EAAA,OAAO,QAAQ,CAAC,CAAA,CAAE,aAAA,CAAc,OAAA,CAAQ,CAAC,CAAC,CAAA;AAC5C;AAGA,SAAS,YAAA,CAAa,GAAoB,CAAA,EAA4B;AACpE,EAAA,MAAM,MAAA,GAAS,cAAA,CAAe,CAAA,EAAG,CAAC,CAAA;AAClC,EAAA,IAAI,MAAA,KAAW,GAAG,OAAO,MAAA;AAGzB,EAAA,OAAO,CAAA,CAAE,SAAA,CAAU,aAAA,CAAc,CAAA,CAAE,SAAS,CAAA;AAC9C;AAEA,SAAS,cAAA,CACP,MACA,MAAA,EACoD;AACpD,EAAA,QAAQ,IAAA;AAAM,IACZ,KAAK,OAAA;AACH,MAAA,OAAO,CAAC,CAAA,EAAG,CAAA,KACT,UAAA,CAAW,OAAO,GAAA,CAAI,CAAA,CAAE,SAAS,CAAC,IAClC,UAAA,CAAW,MAAA,CAAO,GAAA,CAAI,CAAA,CAAE,SAAS,CAAC,CAAA;AAAA,IACtC,KAAK,QAAA;AACH,MAAA,OAAO,CAAC,GAAG,CAAA,KAAM;AAGf,QAAA,MAAM,EAAA,GAAK,aAAa,CAAC,CAAA;AACzB,QAAA,MAAM,EAAA,GAAK,aAAa,CAAC,CAAA;AACzB,QAAA,IAAI,EAAA,KAAO,IAAI,OAAO,CAAA;AACtB,QAAA,OAAO,EAAA,GAAK,KAAK,EAAA,GAAK,CAAA;AAAA,MACxB,CAAA;AAAA,IACF,KAAK,MAAA;AAIH,MAAA,OAAO,cAAA;AAAA,IACT,SAAS;AAIP,MAAA,MAAM,eAAA,GAAyB,IAAA;AAC/B,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,qBAAA,EAAwB,eAAe,CAAA,CAAE,CAAA;AAAA,IAC3D;AAAA;AAEJ;AAgBO,SAAS,aAAA,CACd,SAAA,EACA,IAAA,EACA,SAAA,EACA,MAAA,EACmB;AACnB,EAAA,MAAM,OAAA,GAAU,cAAA,CAAe,IAAA,EAAM,MAAM,CAAA;AAC3C,EAAA,MAAM,IAAA,GAAO,SAAA,KAAc,KAAA,GAAQ,CAAA,GAAI,EAAA;AAEvC,EAAA,OAAO,CAAC,GAAG,SAAS,EAAE,IAAA,CAAK,CAAC,GAAG,CAAA,KAAM;AACnC,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,CAAA,EAAG,CAAC,CAAA;AAC5B,IAAA,IAAI,OAAA,KAAY,CAAA,EAAG,OAAO,IAAA,GAAO,OAAA;AACjC,IAAA,OAAO,YAAA,CAAa,GAAG,CAAC,CAAA;AAAA,EAC1B,CAAC,CAAA;AACH;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.cjs.js","sources":["../src/telemetry.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Telemetry actions (ADR-0007). All four are stored raw and returned raw by\n * `GET /telemetry/:ref` as lifetime totals. `view` is recorded when a user\n * opens a resource, not when a card renders.\n */\nexport const TelemetryActionEnum = z.enum([\n 'install',\n 'copy',\n 'download',\n 'view',\n]);\nexport type TelemetryAction = z.infer<typeof TelemetryActionEnum>;\n\nexport const TelemetryEventInputSchema = z.object({\n ref: z.string().min(1),\n action: TelemetryActionEnum,\n tool: z.string().min(1).optional(),\n});\nexport type TelemetryEventInput = z.infer<typeof TelemetryEventInputSchema>;\n\n/** Raw per-action counts for one resource, as returned by `GET /telemetry/:ref`. */\nexport type TelemetryCounts = Record<TelemetryAction, number>;\n"],"names":["z"],"mappings":";;;;AAOO,MAAM,mBAAA,GAAsBA,MAAE,IAAA,CAAK;AAAA,EACxC,SAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAC;AAGM,MAAM,yBAAA,GAA4BA,MAAE,MAAA,CAAO;AAAA,EAChD,GAAA,EAAKA,KAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EACrB,MAAA,EAAQ,mBAAA;AAAA,EACR,MAAMA,KAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA;AAC1B,CAAC;;;;;"}
1
+ {"version":3,"file":"telemetry.cjs.js","sources":["../src/telemetry.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Telemetry actions (ADR-0007). All four are stored raw and returned raw by\n * `GET /telemetry/:ref` as lifetime totals. `view` is recorded when a user\n * opens a resource, not when a card renders.\n */\nexport const TelemetryActionEnum = z.enum([\n 'install',\n 'copy',\n 'download',\n 'view',\n]);\nexport type TelemetryAction = z.infer<typeof TelemetryActionEnum>;\n\nexport const TelemetryEventInputSchema = z.object({\n ref: z.string().min(1),\n action: TelemetryActionEnum,\n tool: z.string().min(1).optional(),\n});\nexport type TelemetryEventInput = z.infer<typeof TelemetryEventInputSchema>;\n\n/** Raw per-action counts for one resource, as returned by `GET /telemetry/:ref`. */\nexport type TelemetryCounts = Record<TelemetryAction, number>;\n\n/**\n * Raw per-action counts for every resource that has recorded at least one\n * telemetry event, keyed by `entityRef`, as returned by `GET /telemetry`\n * (#83 backoffice overview). A resource with no events at all is simply\n * absent from the map — the caller already has the full resource list and\n * can treat a missing key as zero, so the response only carries entries\n * that mean something.\n */\nexport type TelemetrySummary = Record<string, TelemetryCounts>;\n"],"names":["z"],"mappings":";;;;AAOO,MAAM,mBAAA,GAAsBA,MAAE,IAAA,CAAK;AAAA,EACxC,SAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAC;AAGM,MAAM,yBAAA,GAA4BA,MAAE,MAAA,CAAO;AAAA,EAChD,GAAA,EAAKA,KAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EACrB,MAAA,EAAQ,mBAAA;AAAA,EACR,MAAMA,KAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA;AAC1B,CAAC;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.esm.js","sources":["../src/telemetry.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Telemetry actions (ADR-0007). All four are stored raw and returned raw by\n * `GET /telemetry/:ref` as lifetime totals. `view` is recorded when a user\n * opens a resource, not when a card renders.\n */\nexport const TelemetryActionEnum = z.enum([\n 'install',\n 'copy',\n 'download',\n 'view',\n]);\nexport type TelemetryAction = z.infer<typeof TelemetryActionEnum>;\n\nexport const TelemetryEventInputSchema = z.object({\n ref: z.string().min(1),\n action: TelemetryActionEnum,\n tool: z.string().min(1).optional(),\n});\nexport type TelemetryEventInput = z.infer<typeof TelemetryEventInputSchema>;\n\n/** Raw per-action counts for one resource, as returned by `GET /telemetry/:ref`. */\nexport type TelemetryCounts = Record<TelemetryAction, number>;\n"],"names":[],"mappings":";;AAOO,MAAM,mBAAA,GAAsB,EAAE,IAAA,CAAK;AAAA,EACxC,SAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAC;AAGM,MAAM,yBAAA,GAA4B,EAAE,MAAA,CAAO;AAAA,EAChD,GAAA,EAAK,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EACrB,MAAA,EAAQ,mBAAA;AAAA,EACR,MAAM,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA;AAC1B,CAAC;;;;"}
1
+ {"version":3,"file":"telemetry.esm.js","sources":["../src/telemetry.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Telemetry actions (ADR-0007). All four are stored raw and returned raw by\n * `GET /telemetry/:ref` as lifetime totals. `view` is recorded when a user\n * opens a resource, not when a card renders.\n */\nexport const TelemetryActionEnum = z.enum([\n 'install',\n 'copy',\n 'download',\n 'view',\n]);\nexport type TelemetryAction = z.infer<typeof TelemetryActionEnum>;\n\nexport const TelemetryEventInputSchema = z.object({\n ref: z.string().min(1),\n action: TelemetryActionEnum,\n tool: z.string().min(1).optional(),\n});\nexport type TelemetryEventInput = z.infer<typeof TelemetryEventInputSchema>;\n\n/** Raw per-action counts for one resource, as returned by `GET /telemetry/:ref`. */\nexport type TelemetryCounts = Record<TelemetryAction, number>;\n\n/**\n * Raw per-action counts for every resource that has recorded at least one\n * telemetry event, keyed by `entityRef`, as returned by `GET /telemetry`\n * (#83 backoffice overview). A resource with no events at all is simply\n * absent from the map — the caller already has the full resource list and\n * can treat a missing key as zero, so the response only carries entries\n * that mean something.\n */\nexport type TelemetrySummary = Record<string, TelemetryCounts>;\n"],"names":[],"mappings":";;AAOO,MAAM,mBAAA,GAAsB,EAAE,IAAA,CAAK;AAAA,EACxC,SAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAC;AAGM,MAAM,yBAAA,GAA4B,EAAE,MAAA,CAAO;AAAA,EAChD,GAAA,EAAK,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EACrB,MAAA,EAAQ,mBAAA;AAAA,EACR,MAAM,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA;AAC1B,CAAC;;;;"}
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var catalogModel = require('@backstage/catalog-model');
4
+ var curation = require('./curation.cjs.js');
4
5
  var resources = require('./resources.cjs.js');
5
6
 
6
7
  function normalizeOwner(owner) {
@@ -53,12 +54,14 @@ function toResourceSummary(entity) {
53
54
  }
54
55
  const annotations = entity.metadata.annotations ?? {};
55
56
  const children = declaredChildren(entity);
57
+ const tags = entity.metadata.tags ?? [];
58
+ const recommendedByTag = tags.includes(curation.RECOMMENDED_TAG);
56
59
  return {
57
60
  entityRef: catalogModel.stringifyEntityRef(entity),
58
61
  name: entity.metadata.name,
59
62
  title: entity.metadata.title,
60
63
  description: entity.metadata.description,
61
- tags: entity.metadata.tags ?? [],
64
+ tags,
62
65
  type,
63
66
  lifecycle: typeof entity.spec?.lifecycle === "string" ? entity.spec.lifecycle : "",
64
67
  owner: typeof entity.spec?.owner === "string" ? normalizeOwner(entity.spec.owner) : void 0,
@@ -73,7 +76,8 @@ function toResourceSummary(entity) {
73
76
  parents: [],
74
77
  childCount: children.length,
75
78
  helpText: annotations[resources.ANNOTATION_HELP],
76
- annotations
79
+ annotations,
80
+ ...recommendedByTag && { recommended: true, recommendedByTag: true }
77
81
  };
78
82
  }
79
83
  function toResourceSummaries(entities, opts = {}) {
@@ -92,14 +96,26 @@ function toResourceSummaries(entities, opts = {}) {
92
96
  else parentsByChild.set(childRef, [summary.entityRef]);
93
97
  }
94
98
  }
95
- const allowList = opts.allowedFrameworks;
99
+ const custom = (opts.customFrameworks ?? []).map(resources.normalizeFramework).filter(Boolean);
100
+ const customTokens = new Set(custom);
101
+ const allowList = resources.resolveAllowedFrameworks(opts.allowedFrameworks, custom);
96
102
  for (const summary of summaries) {
97
103
  summary.parents = parentsByChild.get(summary.entityRef) ?? [];
98
104
  const declared = summary.frameworks;
99
- summary.frameworks = resources.filterAllowedFrameworks(declared, allowList);
105
+ summary.frameworks = resources.filterAllowedFrameworks(declared, allowList, custom);
100
106
  summary.frameworksRestricted = allowList !== void 0 && summary.frameworks.length === 0 && (declared.length > 0 || allowList.length === 0);
101
107
  if (allowList !== void 0 && declared.length === 0) {
102
- summary.installFrameworks = resources.filterAllowedFrameworks(["all"], allowList);
108
+ summary.installFrameworks = resources.filterAllowedFrameworks(
109
+ ["all"],
110
+ allowList,
111
+ custom
112
+ );
113
+ } else if (summary.frameworks.some((f) => customTokens.has(f))) {
114
+ const installable = summary.frameworks.filter((f) => !customTokens.has(f));
115
+ summary.installFrameworks = installable;
116
+ if (installable.length === 0) {
117
+ summary.frameworksRestricted = true;
118
+ }
103
119
  }
104
120
  }
105
121
  return summaries;
@@ -1 +1 @@
1
- {"version":3,"file":"toResourceSummary.cjs.js","sources":["../src/toResourceSummary.ts"],"sourcesContent":["import {\n ANNOTATION_SOURCE_LOCATION,\n parseEntityRef,\n stringifyEntityRef,\n type Entity,\n} from '@backstage/catalog-model';\nimport {\n ANNOTATION_HELP,\n filterAllowedFrameworks,\n getFrameworks,\n isRenderableContainment,\n isResourceType,\n type ResourceSummary,\n} from './resources';\n\n/**\n * Map a catalog AiResource entity to the flat contract served to the\n * frontend. Returns undefined for entities with an unsupported `spec.type`,\n * which are silently dropped (ADR-0003).\n *\n * Lives in `common` rather than the backend because it is pure and\n * isomorphic — `Entity` in, `ResourceSummary` out — and both the backend\n * router and the frontend's e2e fixtures need it to agree. The e2e suite\n * builds its mock `GET /resources` payload by running the real examples in\n * `examples/catalog/` through this function, so a change to the mapping\n * cannot silently diverge from what the tests assert against.\n */\n/**\n * Normalise `spec.owner` to a full entity ref. Producers write the short form\n * (`group:platforms-developer-experience`, or a bare group name), while\n * anything that compares or links refs needs the canonical\n * `group:default/platforms-developer-experience`.\n *\n * An unparseable ref passes through untouched: the owner is a display field,\n * never worth failing the whole resource over.\n */\nfunction normalizeOwner(owner: string): string {\n try {\n return stringifyEntityRef(\n parseEntityRef(owner, {\n defaultKind: 'group',\n defaultNamespace: 'default',\n }),\n );\n } catch {\n return owner;\n }\n}\n\n/**\n * Whether a string is a parseable entity ref. The frontend links `owner` to\n * its catalog page (via `EntityRefLink`, which throws on an unparseable\n * ref) — this lets it fall back to plain text instead, matching\n * `normalizeOwner`'s own stance that a malformed owner is never worth\n * failing over.\n */\nexport function isParseableEntityRef(ref: string): boolean {\n try {\n parseEntityRef(ref, { defaultKind: 'group', defaultNamespace: 'default' });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Normalise a declared child ref to the canonical, lowercased form\n * `stringifyEntityRef` emits, so it compares equal to a container entity's own\n * ref regardless of how the producer wrote it (bare name or full ref). A bare\n * name resolves in the *container's own* namespace — mirroring Backstage's\n * native relation fields, which declare `defaultNamespace: 'inherit'` — not\n * always `default`; a plugin in `team-a` listing a bare `child` must resolve\n * to `airesource:team-a/child`, or it silently loses the relationship. An\n * unparseable ref is dropped: a member the catalog could never resolve is not\n * a relationship worth carrying.\n */\nfunction normalizeChildRef(\n ref: unknown,\n containerNamespace: string,\n): string | undefined {\n if (typeof ref !== 'string') return undefined;\n try {\n return stringifyEntityRef(\n parseEntityRef(ref, {\n defaultKind: 'AiResource',\n defaultNamespace: containerNamespace,\n }),\n );\n } catch {\n return undefined;\n }\n}\n\n/**\n * The refs a container declares as its contents: a `marketplace`'s\n * `spec.plugins`, a `plugin`'s `spec.skills` (ADR-0015). Leaf types declare\n * nothing. Unparseable and duplicate refs are dropped; visibility filtering\n * happens in `toResourceSummaries`, which alone sees the whole read.\n */\nfunction declaredChildren(entity: Entity): string[] {\n const type = entity.spec?.type;\n let raw: unknown;\n if (type === 'marketplace') raw = entity.spec?.plugins;\n else if (type === 'plugin') raw = entity.spec?.skills;\n if (!Array.isArray(raw)) return [];\n const containerNamespace = entity.metadata.namespace ?? 'default';\n const refs = raw\n .map(ref => normalizeChildRef(ref, containerNamespace))\n .filter((r): r is string => r !== undefined);\n return Array.from(new Set(refs));\n}\n\nexport function toResourceSummary(entity: Entity): ResourceSummary | undefined {\n const type = entity.spec?.type;\n if (!isResourceType(type)) {\n return undefined;\n }\n\n const annotations = entity.metadata.annotations ?? {};\n const children = declaredChildren(entity);\n\n return {\n entityRef: stringifyEntityRef(entity),\n name: entity.metadata.name,\n title: entity.metadata.title,\n description: entity.metadata.description,\n tags: entity.metadata.tags ?? [],\n type,\n lifecycle:\n typeof entity.spec?.lifecycle === 'string' ? entity.spec.lifecycle : '',\n owner:\n typeof entity.spec?.owner === 'string'\n ? normalizeOwner(entity.spec.owner)\n : undefined,\n sourceLocation: annotations[ANNOTATION_SOURCE_LOCATION],\n frameworks: getFrameworks(entity),\n version:\n typeof entity.spec?.version === 'string'\n ? entity.spec.version\n : undefined,\n kind: entity.kind,\n // `parents` needs the whole read to invert; a single entity can't know\n // them, so `toResourceSummaries` fills them in. `children` here is still\n // the raw declared set — visibility filtering also needs the whole read.\n children,\n parents: [],\n childCount: children.length,\n helpText: annotations[ANNOTATION_HELP],\n annotations,\n };\n}\n\n/**\n * Map a whole caller-visible catalog read to summaries, resolving containment\n * in both directions (ADR-0015). This is the function the served payload uses;\n * `toResourceSummary` alone cannot, because containment is only knowable across\n * the whole set.\n *\n * Upstream imposes no type restriction on membership — `spec.skills` /\n * `spec.plugins` carry `allowedKinds: [\"AiResource\"]`, so any AiResource type\n * is a legal member. DevAI Hub narrows that to its rendering convention\n * (ADR-0010/0015): a `marketplace` renders `plugin` children only, a `plugin`\n * renders `skill`/`agent`/`hook`/`mcp-config`. That convention is applied here,\n * once, so `children`, `childCount`, `parents`, the card chip and the detail\n * sections cannot disagree:\n *\n * - `children` keeps only refs that resolve within this read **and** whose type\n * the container legitimately contains — so a card never counts, nor a detail\n * panel links, a hidden member (ADR-0006) or an off-convention one.\n * - `parents` is the in-memory inverse of those filtered children.\n *\n * `opts.allowedFrameworks` (from `devAiHub.frameworks.allowed`) narrows every\n * summary's `frameworks` to the permitted set — undefined/empty allows all.\n * Applied here so the served payload is the single choke point: badges, the\n * \"Works with\" section, the framework filter and install-path generation all\n * read the already-filtered list.\n */\nexport function toResourceSummaries(\n entities: Entity[],\n opts: { allowedFrameworks?: string[] } = {},\n): ResourceSummary[] {\n const summaries = entities\n .map(toResourceSummary)\n .filter((s): s is ResourceSummary => s !== undefined);\n\n const byRef = new Map(summaries.map(s => [s.entityRef, s]));\n const parentsByChild = new Map<string, string[]>();\n\n for (const summary of summaries) {\n summary.children = summary.children.filter(ref => {\n const child = byRef.get(ref);\n return (\n child !== undefined && isRenderableContainment(summary.type, child.type)\n );\n });\n summary.childCount = summary.children.length;\n for (const childRef of summary.children) {\n const bucket = parentsByChild.get(childRef);\n if (bucket) bucket.push(summary.entityRef);\n else parentsByChild.set(childRef, [summary.entityRef]);\n }\n }\n\n const allowList = opts.allowedFrameworks;\n for (const summary of summaries) {\n summary.parents = parentsByChild.get(summary.entityRef) ?? [];\n const declared = summary.frameworks;\n summary.frameworks = filterAllowedFrameworks(declared, allowList);\n // \"Restricted\" means an active allow-list left the resource with zero\n // frameworks and the UI must suppress generated install paths rather than\n // fall back to the neutral `default` an empty list otherwise triggers.\n // Two cases qualify: the allow-list removed frameworks the author had, or\n // the allow-list itself is empty (the deployment opted out of frameworks\n // wholesale). A resource that declared nothing under a *non-empty* list\n // keeps its neutral default — it has no disallowed claim to hide.\n summary.frameworksRestricted =\n allowList !== undefined &&\n summary.frameworks.length === 0 &&\n (declared.length > 0 || allowList.length === 0);\n // A resource that declared no frameworks means \"every host\", so its\n // launchers/commands must still be scoped to an active allow-list — carry\n // the effective set separately so it never surfaces as a badge.\n if (allowList !== undefined && declared.length === 0) {\n summary.installFrameworks = filterAllowedFrameworks(['all'], allowList);\n }\n }\n\n return summaries;\n}\n"],"names":["stringifyEntityRef","parseEntityRef","isResourceType","ANNOTATION_SOURCE_LOCATION","getFrameworks","ANNOTATION_HELP","isRenderableContainment","filterAllowedFrameworks"],"mappings":"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"}
1
+ {"version":3,"file":"toResourceSummary.cjs.js","sources":["../src/toResourceSummary.ts"],"sourcesContent":["import {\n ANNOTATION_SOURCE_LOCATION,\n parseEntityRef,\n stringifyEntityRef,\n type Entity,\n} from '@backstage/catalog-model';\nimport { RECOMMENDED_TAG } from './curation';\nimport {\n ANNOTATION_HELP,\n filterAllowedFrameworks,\n getFrameworks,\n isRenderableContainment,\n isResourceType,\n normalizeFramework,\n resolveAllowedFrameworks,\n type ResourceSummary,\n} from './resources';\n\n/**\n * Map a catalog AiResource entity to the flat contract served to the\n * frontend. Returns undefined for entities with an unsupported `spec.type`,\n * which are silently dropped (ADR-0003).\n *\n * Lives in `common` rather than the backend because it is pure and\n * isomorphic — `Entity` in, `ResourceSummary` out — and both the backend\n * router and the frontend's e2e fixtures need it to agree. The e2e suite\n * builds its mock `GET /resources` payload by running the real examples in\n * `examples/catalog/` through this function, so a change to the mapping\n * cannot silently diverge from what the tests assert against.\n */\n/**\n * Normalise `spec.owner` to a full entity ref. Producers write the short form\n * (`group:platforms-developer-experience`, or a bare group name), while\n * anything that compares or links refs needs the canonical\n * `group:default/platforms-developer-experience`.\n *\n * An unparseable ref passes through untouched: the owner is a display field,\n * never worth failing the whole resource over.\n */\nfunction normalizeOwner(owner: string): string {\n try {\n return stringifyEntityRef(\n parseEntityRef(owner, {\n defaultKind: 'group',\n defaultNamespace: 'default',\n }),\n );\n } catch {\n return owner;\n }\n}\n\n/**\n * Whether a string is a parseable entity ref. The frontend links `owner` to\n * its catalog page (via `EntityRefLink`, which throws on an unparseable\n * ref) — this lets it fall back to plain text instead, matching\n * `normalizeOwner`'s own stance that a malformed owner is never worth\n * failing over.\n */\nexport function isParseableEntityRef(ref: string): boolean {\n try {\n parseEntityRef(ref, { defaultKind: 'group', defaultNamespace: 'default' });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Normalise a declared child ref to the canonical, lowercased form\n * `stringifyEntityRef` emits, so it compares equal to a container entity's own\n * ref regardless of how the producer wrote it (bare name or full ref). A bare\n * name resolves in the *container's own* namespace — mirroring Backstage's\n * native relation fields, which declare `defaultNamespace: 'inherit'` — not\n * always `default`; a plugin in `team-a` listing a bare `child` must resolve\n * to `airesource:team-a/child`, or it silently loses the relationship. An\n * unparseable ref is dropped: a member the catalog could never resolve is not\n * a relationship worth carrying.\n */\nfunction normalizeChildRef(\n ref: unknown,\n containerNamespace: string,\n): string | undefined {\n if (typeof ref !== 'string') return undefined;\n try {\n return stringifyEntityRef(\n parseEntityRef(ref, {\n defaultKind: 'AiResource',\n defaultNamespace: containerNamespace,\n }),\n );\n } catch {\n return undefined;\n }\n}\n\n/**\n * The refs a container declares as its contents: a `marketplace`'s\n * `spec.plugins`, a `plugin`'s `spec.skills` (ADR-0015). Leaf types declare\n * nothing. Unparseable and duplicate refs are dropped; visibility filtering\n * happens in `toResourceSummaries`, which alone sees the whole read.\n */\nfunction declaredChildren(entity: Entity): string[] {\n const type = entity.spec?.type;\n let raw: unknown;\n if (type === 'marketplace') raw = entity.spec?.plugins;\n else if (type === 'plugin') raw = entity.spec?.skills;\n if (!Array.isArray(raw)) return [];\n const containerNamespace = entity.metadata.namespace ?? 'default';\n const refs = raw\n .map(ref => normalizeChildRef(ref, containerNamespace))\n .filter((r): r is string => r !== undefined);\n return Array.from(new Set(refs));\n}\n\nexport function toResourceSummary(entity: Entity): ResourceSummary | undefined {\n const type = entity.spec?.type;\n if (!isResourceType(type)) {\n return undefined;\n }\n\n const annotations = entity.metadata.annotations ?? {};\n const children = declaredChildren(entity);\n const tags = entity.metadata.tags ?? [];\n // An author-declared recommendation (ADR-0020) is a fact the catalog holds,\n // so it is mapped here; the curation overlay (ADR-0017) can only add to it.\n const recommendedByTag = tags.includes(RECOMMENDED_TAG);\n\n return {\n entityRef: stringifyEntityRef(entity),\n name: entity.metadata.name,\n title: entity.metadata.title,\n description: entity.metadata.description,\n tags,\n type,\n lifecycle:\n typeof entity.spec?.lifecycle === 'string' ? entity.spec.lifecycle : '',\n owner:\n typeof entity.spec?.owner === 'string'\n ? normalizeOwner(entity.spec.owner)\n : undefined,\n sourceLocation: annotations[ANNOTATION_SOURCE_LOCATION],\n frameworks: getFrameworks(entity),\n version:\n typeof entity.spec?.version === 'string'\n ? entity.spec.version\n : undefined,\n kind: entity.kind,\n // `parents` needs the whole read to invert; a single entity can't know\n // them, so `toResourceSummaries` fills them in. `children` here is still\n // the raw declared set — visibility filtering also needs the whole read.\n children,\n parents: [],\n childCount: children.length,\n helpText: annotations[ANNOTATION_HELP],\n annotations,\n ...(recommendedByTag && { recommended: true, recommendedByTag: true }),\n };\n}\n\n/**\n * Map a whole caller-visible catalog read to summaries, resolving containment\n * in both directions (ADR-0015). This is the function the served payload uses;\n * `toResourceSummary` alone cannot, because containment is only knowable across\n * the whole set.\n *\n * Upstream imposes no type restriction on membership — `spec.skills` /\n * `spec.plugins` carry `allowedKinds: [\"AiResource\"]`, so any AiResource type\n * is a legal member. DevAI Hub narrows that to its rendering convention\n * (ADR-0010/0015): a `marketplace` renders `plugin` children only, a `plugin`\n * renders `skill`/`agent`/`hook`/`mcp-config`. That convention is applied here,\n * once, so `children`, `childCount`, `parents`, the card chip and the detail\n * sections cannot disagree:\n *\n * - `children` keeps only refs that resolve within this read **and** whose type\n * the container legitimately contains — so a card never counts, nor a detail\n * panel links, a hidden member (ADR-0006) or an off-convention one.\n * - `parents` is the in-memory inverse of those filtered children.\n *\n * `opts.allowedFrameworks` (from `devAiHub.frameworks.allowed`) narrows every\n * summary's `frameworks` to the permitted set — undefined/empty allows all.\n * `opts.customFrameworks` (the keys of `devAiHub.frameworks.custom`) joins\n * that set and, on its own, activates it (ADR-0018). Applied here so the\n * served payload is the single choke point: badges, the \"Works with\" section,\n * the framework filter and install-path generation all read the\n * already-filtered list.\n */\nexport function toResourceSummaries(\n entities: Entity[],\n opts: { allowedFrameworks?: string[]; customFrameworks?: string[] } = {},\n): ResourceSummary[] {\n const summaries = entities\n .map(toResourceSummary)\n .filter((s): s is ResourceSummary => s !== undefined);\n\n const byRef = new Map(summaries.map(s => [s.entityRef, s]));\n const parentsByChild = new Map<string, string[]>();\n\n for (const summary of summaries) {\n summary.children = summary.children.filter(ref => {\n const child = byRef.get(ref);\n return (\n child !== undefined && isRenderableContainment(summary.type, child.type)\n );\n });\n summary.childCount = summary.children.length;\n for (const childRef of summary.children) {\n const bucket = parentsByChild.get(childRef);\n if (bucket) bucket.push(summary.entityRef);\n else parentsByChild.set(childRef, [summary.entityRef]);\n }\n }\n\n const custom = (opts.customFrameworks ?? [])\n .map(normalizeFramework)\n .filter(Boolean);\n const customTokens = new Set(custom);\n const allowList = resolveAllowedFrameworks(opts.allowedFrameworks, custom);\n for (const summary of summaries) {\n summary.parents = parentsByChild.get(summary.entityRef) ?? [];\n const declared = summary.frameworks;\n summary.frameworks = filterAllowedFrameworks(declared, allowList, custom);\n // \"Restricted\" means an active allow-list left the resource with zero\n // frameworks and the UI must suppress generated install paths rather than\n // fall back to the neutral `default` an empty list otherwise triggers.\n // Two cases qualify: the allow-list removed frameworks the author had, or\n // the allow-list itself is empty (the deployment opted out of frameworks\n // wholesale). A resource that declared nothing under a *non-empty* list\n // keeps its neutral default — it has no disallowed claim to hide.\n summary.frameworksRestricted =\n allowList !== undefined &&\n summary.frameworks.length === 0 &&\n (declared.length > 0 || allowList.length === 0);\n // A resource that declared no frameworks means \"every host\", so its\n // launchers/commands must still be scoped to an active allow-list — carry\n // the effective set separately so it never surfaces as a badge.\n if (allowList !== undefined && declared.length === 0) {\n summary.installFrameworks = filterAllowedFrameworks(\n ['all'],\n allowList,\n custom,\n );\n } else if (summary.frameworks.some(f => customTokens.has(f))) {\n // Custom frameworks are discovery-only (ADR-0018): the plugin knows no\n // filesystem convention for them, and `getResourceInstallTarget` would\n // otherwise hand them the neutral `default` path — a concrete claim\n // about a tool we know nothing about. Scope install generation to what\n // is left, and when nothing is, reuse the restricted suppression so the\n // dialog says so instead of rendering that default.\n const installable = summary.frameworks.filter(f => !customTokens.has(f));\n summary.installFrameworks = installable;\n if (installable.length === 0) {\n summary.frameworksRestricted = true;\n }\n }\n }\n\n return summaries;\n}\n"],"names":["stringifyEntityRef","parseEntityRef","isResourceType","RECOMMENDED_TAG","ANNOTATION_SOURCE_LOCATION","getFrameworks","ANNOTATION_HELP","isRenderableContainment","normalizeFramework","resolveAllowedFrameworks","filterAllowedFrameworks"],"mappings":"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"}