@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.
- package/README.md +3 -1
- package/config.schema.json +34 -0
- package/dist/curation.cjs.js +16 -0
- package/dist/curation.cjs.js.map +1 -0
- package/dist/curation.esm.js +12 -0
- package/dist/curation.esm.js.map +1 -0
- package/dist/index.cjs.js +18 -0
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +279 -23
- package/dist/index.esm.js +4 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/permissions.cjs.js +21 -0
- package/dist/permissions.cjs.js.map +1 -0
- package/dist/permissions.esm.js +17 -0
- package/dist/permissions.esm.js.map +1 -0
- package/dist/resources.cjs.js +49 -2
- package/dist/resources.cjs.js.map +1 -1
- package/dist/resources.esm.js +48 -3
- package/dist/resources.esm.js.map +1 -1
- package/dist/sorting.cjs.js +71 -0
- package/dist/sorting.cjs.js.map +1 -0
- package/dist/sorting.esm.js +63 -0
- package/dist/sorting.esm.js.map +1 -0
- package/dist/telemetry.cjs.js.map +1 -1
- package/dist/telemetry.esm.js.map +1 -1
- package/dist/toResourceSummary.cjs.js +21 -5
- package/dist/toResourceSummary.cjs.js.map +1 -1
- package/dist/toResourceSummary.esm.js +22 -6
- package/dist/toResourceSummary.esm.js.map +1 -1
- package/package.json +4 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseEntityRef, ANNOTATION_SOURCE_LOCATION, stringifyEntityRef } from '@backstage/catalog-model';
|
|
2
|
-
import {
|
|
2
|
+
import { RECOMMENDED_TAG } from './curation.esm.js';
|
|
3
|
+
import { isRenderableContainment, normalizeFramework, resolveAllowedFrameworks, filterAllowedFrameworks, isResourceType, ANNOTATION_HELP, getFrameworks } from './resources.esm.js';
|
|
3
4
|
|
|
4
5
|
function normalizeOwner(owner) {
|
|
5
6
|
try {
|
|
@@ -51,12 +52,14 @@ function toResourceSummary(entity) {
|
|
|
51
52
|
}
|
|
52
53
|
const annotations = entity.metadata.annotations ?? {};
|
|
53
54
|
const children = declaredChildren(entity);
|
|
55
|
+
const tags = entity.metadata.tags ?? [];
|
|
56
|
+
const recommendedByTag = tags.includes(RECOMMENDED_TAG);
|
|
54
57
|
return {
|
|
55
58
|
entityRef: stringifyEntityRef(entity),
|
|
56
59
|
name: entity.metadata.name,
|
|
57
60
|
title: entity.metadata.title,
|
|
58
61
|
description: entity.metadata.description,
|
|
59
|
-
tags
|
|
62
|
+
tags,
|
|
60
63
|
type,
|
|
61
64
|
lifecycle: typeof entity.spec?.lifecycle === "string" ? entity.spec.lifecycle : "",
|
|
62
65
|
owner: typeof entity.spec?.owner === "string" ? normalizeOwner(entity.spec.owner) : void 0,
|
|
@@ -71,7 +74,8 @@ function toResourceSummary(entity) {
|
|
|
71
74
|
parents: [],
|
|
72
75
|
childCount: children.length,
|
|
73
76
|
helpText: annotations[ANNOTATION_HELP],
|
|
74
|
-
annotations
|
|
77
|
+
annotations,
|
|
78
|
+
...recommendedByTag && { recommended: true, recommendedByTag: true }
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
function toResourceSummaries(entities, opts = {}) {
|
|
@@ -90,14 +94,26 @@ function toResourceSummaries(entities, opts = {}) {
|
|
|
90
94
|
else parentsByChild.set(childRef, [summary.entityRef]);
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
|
-
const
|
|
97
|
+
const custom = (opts.customFrameworks ?? []).map(normalizeFramework).filter(Boolean);
|
|
98
|
+
const customTokens = new Set(custom);
|
|
99
|
+
const allowList = resolveAllowedFrameworks(opts.allowedFrameworks, custom);
|
|
94
100
|
for (const summary of summaries) {
|
|
95
101
|
summary.parents = parentsByChild.get(summary.entityRef) ?? [];
|
|
96
102
|
const declared = summary.frameworks;
|
|
97
|
-
summary.frameworks = filterAllowedFrameworks(declared, allowList);
|
|
103
|
+
summary.frameworks = filterAllowedFrameworks(declared, allowList, custom);
|
|
98
104
|
summary.frameworksRestricted = allowList !== void 0 && summary.frameworks.length === 0 && (declared.length > 0 || allowList.length === 0);
|
|
99
105
|
if (allowList !== void 0 && declared.length === 0) {
|
|
100
|
-
summary.installFrameworks = filterAllowedFrameworks(
|
|
106
|
+
summary.installFrameworks = filterAllowedFrameworks(
|
|
107
|
+
["all"],
|
|
108
|
+
allowList,
|
|
109
|
+
custom
|
|
110
|
+
);
|
|
111
|
+
} else if (summary.frameworks.some((f) => customTokens.has(f))) {
|
|
112
|
+
const installable = summary.frameworks.filter((f) => !customTokens.has(f));
|
|
113
|
+
summary.installFrameworks = installable;
|
|
114
|
+
if (installable.length === 0) {
|
|
115
|
+
summary.frameworksRestricted = true;
|
|
116
|
+
}
|
|
101
117
|
}
|
|
102
118
|
}
|
|
103
119
|
return summaries;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toResourceSummary.esm.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":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"toResourceSummary.esm.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":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nospt/plugin-dev-ai-hub-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-rc1",
|
|
4
4
|
"description": "Common types and schemas for Dev AI Hub",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.cjs.js",
|
|
@@ -40,8 +40,10 @@
|
|
|
40
40
|
"sideEffects": false,
|
|
41
41
|
"files": [
|
|
42
42
|
"dist",
|
|
43
|
+
"config.schema.json",
|
|
43
44
|
"README.md"
|
|
44
45
|
],
|
|
46
|
+
"configSchema": "config.schema.json",
|
|
45
47
|
"scripts": {
|
|
46
48
|
"build": "backstage-cli package build",
|
|
47
49
|
"clean": "backstage-cli package clean",
|
|
@@ -53,6 +55,7 @@
|
|
|
53
55
|
},
|
|
54
56
|
"dependencies": {
|
|
55
57
|
"@backstage/catalog-model": "^1.10.0",
|
|
58
|
+
"@backstage/plugin-permission-common": "^0.9.10",
|
|
56
59
|
"zod": "^3.22.0"
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|