@nospt/plugin-dev-ai-hub 0.1.0

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.
Files changed (55) hide show
  1. package/.eslintrc.js +1 -0
  2. package/dev/index.tsx +232 -0
  3. package/dist/api/DevAiHubClient.esm.js +76 -0
  4. package/dist/api/DevAiHubClient.esm.js.map +1 -0
  5. package/dist/components/AssetCard/AssetCard.esm.js +194 -0
  6. package/dist/components/AssetCard/AssetCard.esm.js.map +1 -0
  7. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +324 -0
  8. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +1 -0
  9. package/dist/components/AssetFilters/AssetFilters.esm.js +216 -0
  10. package/dist/components/AssetFilters/AssetFilters.esm.js.map +1 -0
  11. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +194 -0
  12. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +1 -0
  13. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +320 -0
  14. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +1 -0
  15. package/dist/components/DevAiHubPage/index.esm.js +6 -0
  16. package/dist/components/DevAiHubPage/index.esm.js.map +1 -0
  17. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js +274 -0
  18. package/dist/components/McpConfigDialog/McpConfigDialog.esm.js.map +1 -0
  19. package/dist/components/ToolIcon/ToolIcon.esm.js +41 -0
  20. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -0
  21. package/dist/hooks/index.esm.js +114 -0
  22. package/dist/hooks/index.esm.js.map +1 -0
  23. package/dist/index.d.ts +104 -0
  24. package/dist/index.esm.js +5 -0
  25. package/dist/index.esm.js.map +1 -0
  26. package/dist/plugin.esm.js +40 -0
  27. package/dist/plugin.esm.js.map +1 -0
  28. package/dist/pluginLegacy.esm.js +27 -0
  29. package/dist/pluginLegacy.esm.js.map +1 -0
  30. package/dist/routes.esm.js +6 -0
  31. package/dist/routes.esm.js.map +1 -0
  32. package/package.json +80 -0
  33. package/package.json-prepack +75 -0
  34. package/src/api/DevAiHubClient.ts +110 -0
  35. package/src/components/AssetCard/AssetCard.test.tsx +182 -0
  36. package/src/components/AssetCard/AssetCard.tsx +221 -0
  37. package/src/components/AssetCard/index.ts +1 -0
  38. package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +377 -0
  39. package/src/components/AssetDetailPanel/index.ts +1 -0
  40. package/src/components/AssetFilters/AssetFilters.tsx +261 -0
  41. package/src/components/AssetFilters/index.ts +2 -0
  42. package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +216 -0
  43. package/src/components/AssetInstallDialog/index.ts +1 -0
  44. package/src/components/DevAiHubPage/DevAiHubPage.tsx +346 -0
  45. package/src/components/DevAiHubPage/index.ts +1 -0
  46. package/src/components/McpConfigDialog/McpConfigDialog.tsx +301 -0
  47. package/src/components/McpConfigDialog/index.ts +1 -0
  48. package/src/components/ToolIcon/ToolIcon.tsx +49 -0
  49. package/src/components/ToolIcon/index.ts +1 -0
  50. package/src/hooks/index.ts +152 -0
  51. package/src/index.ts +10 -0
  52. package/src/plugin.tsx +47 -0
  53. package/src/pluginLegacy.ts +42 -0
  54. package/src/routes.ts +3 -0
  55. package/tsconfig.json +10 -0
@@ -0,0 +1,104 @@
1
+ import * as react from 'react';
2
+ import { ComponentType } from 'react';
3
+ import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
4
+ import { AssetListFilter, AiAssetListResponse, AiAsset, AiHubProvider, AiHubStats } from '@nospt/plugin-dev-ai-hub-common';
5
+
6
+ declare const devAiHubPlugin: _backstage_frontend_plugin_api.OverridableFrontendPlugin<{
7
+ root: _backstage_frontend_plugin_api.RouteRef<undefined>;
8
+ }, {}, {
9
+ "api:dev-ai-hub": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
10
+ kind: "api";
11
+ name: undefined;
12
+ config: {};
13
+ configInput: {};
14
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.AnyApiFactory, "core.api.factory", {}>;
15
+ inputs: {};
16
+ params: <TApi, TImpl extends TApi, TDeps extends { [name in string]: unknown; }>(params: _backstage_frontend_plugin_api.ApiFactory<TApi, TImpl, TDeps>) => _backstage_frontend_plugin_api.ExtensionBlueprintParams<_backstage_frontend_plugin_api.AnyApiFactory>;
17
+ }>;
18
+ "nav-item:dev-ai-hub": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
19
+ kind: "nav-item";
20
+ name: undefined;
21
+ config: {};
22
+ configInput: {};
23
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<{
24
+ title: string;
25
+ icon: _backstage_frontend_plugin_api.IconComponent;
26
+ routeRef: _backstage_frontend_plugin_api.RouteRef<undefined>;
27
+ }, "core.nav-item.target", {}>;
28
+ inputs: {};
29
+ params: {
30
+ title: string;
31
+ icon: _backstage_frontend_plugin_api.IconComponent;
32
+ routeRef: _backstage_frontend_plugin_api.RouteRef<undefined>;
33
+ };
34
+ }>;
35
+ "page:dev-ai-hub": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
36
+ kind: "page";
37
+ name: undefined;
38
+ config: {
39
+ path: string | undefined;
40
+ title: string | undefined;
41
+ };
42
+ configInput: {
43
+ title?: string | undefined;
44
+ path?: string | undefined;
45
+ };
46
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
47
+ optional: true;
48
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.title", {
49
+ optional: true;
50
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
51
+ optional: true;
52
+ }>;
53
+ inputs: {
54
+ pages: _backstage_frontend_plugin_api.ExtensionInput<_backstage_frontend_plugin_api.ConfigurableExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
55
+ optional: true;
56
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.title", {
57
+ optional: true;
58
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
59
+ optional: true;
60
+ }>, {
61
+ singleton: false;
62
+ optional: false;
63
+ internal: false;
64
+ }>;
65
+ };
66
+ params: {
67
+ defaultPath?: [Error: `Use the 'path' param instead`];
68
+ path: string;
69
+ title?: string;
70
+ icon?: _backstage_frontend_plugin_api.IconElement;
71
+ loader?: () => Promise<react.JSX.Element>;
72
+ routeRef?: _backstage_frontend_plugin_api.RouteRef;
73
+ noHeader?: boolean;
74
+ };
75
+ }>;
76
+ }>;
77
+
78
+ /**
79
+ * Legacy Backstage frontend system support.
80
+ *
81
+ * For New Frontend System (NFS) use `devAiHubPlugin` from `./plugin` instead.
82
+ */
83
+
84
+ /** Routable page extension for the legacy frontend system. */
85
+ declare const DevAiHubPage: ComponentType;
86
+
87
+ declare const devAiHubApiRef: _backstage_frontend_plugin_api.ApiRef<DevAiHubApi>;
88
+ interface DevAiHubApi {
89
+ listAssets(filter?: AssetListFilter): Promise<AiAssetListResponse>;
90
+ getAsset(id: string): Promise<AiAsset>;
91
+ getAssetRaw(id: string): Promise<string>;
92
+ /** Returns the absolute URL for the download endpoint (zip for skills, md for others). */
93
+ getDownloadUrl(id: string): Promise<string>;
94
+ trackInstall(id: string): Promise<void>;
95
+ listProviders(): Promise<AiHubProvider[]>;
96
+ getProviderStatus(id: string): Promise<AiHubProvider>;
97
+ triggerSync(id: string): Promise<void>;
98
+ getStats(): Promise<AiHubStats>;
99
+ }
100
+
101
+ declare const rootRouteRef: _backstage_frontend_plugin_api.RouteRef<undefined>;
102
+
103
+ export { DevAiHubPage, devAiHubApiRef, devAiHubPlugin, rootRouteRef };
104
+ export type { DevAiHubApi };
@@ -0,0 +1,5 @@
1
+ export { devAiHubPlugin } from './plugin.esm.js';
2
+ export { DevAiHubPage } from './pluginLegacy.esm.js';
3
+ export { devAiHubApiRef } from './api/DevAiHubClient.esm.js';
4
+ export { rootRouteRef } from './routes.esm.js';
5
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
@@ -0,0 +1,40 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { createFrontendPlugin, ApiBlueprint, createApiFactory, fetchApiRef, discoveryApiRef, PageBlueprint, NavItemBlueprint } from '@backstage/frontend-plugin-api';
3
+ import HubIcon from '@mui/icons-material/Hub';
4
+ import { DevAiHubClient, devAiHubApiRef } from './api/DevAiHubClient.esm.js';
5
+ import { rootRouteRef } from './routes.esm.js';
6
+
7
+ const devAiHubPlugin = createFrontendPlugin({
8
+ pluginId: "dev-ai-hub",
9
+ routes: {
10
+ root: rootRouteRef
11
+ },
12
+ extensions: [
13
+ ApiBlueprint.make({
14
+ params: (defineParams) => defineParams(
15
+ createApiFactory({
16
+ api: devAiHubApiRef,
17
+ deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
18
+ factory: ({ discoveryApi, fetchApi }) => new DevAiHubClient(discoveryApi, fetchApi)
19
+ })
20
+ )
21
+ }),
22
+ PageBlueprint.make({
23
+ params: {
24
+ path: "/dev-ai-hub",
25
+ routeRef: rootRouteRef,
26
+ loader: () => import('./components/DevAiHubPage/index.esm.js').then((m) => /* @__PURE__ */ jsx(m.DevAiHubPage, {}))
27
+ }
28
+ }),
29
+ NavItemBlueprint.make({
30
+ params: {
31
+ title: "AI Hub",
32
+ routeRef: rootRouteRef,
33
+ icon: HubIcon
34
+ }
35
+ })
36
+ ]
37
+ });
38
+
39
+ export { devAiHubPlugin };
40
+ //# sourceMappingURL=plugin.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["import {\n createFrontendPlugin,\n PageBlueprint,\n ApiBlueprint,\n NavItemBlueprint,\n createApiFactory,\n discoveryApiRef,\n fetchApiRef,\n} from '@backstage/frontend-plugin-api';\nimport HubIcon from '@mui/icons-material/Hub';\nimport { devAiHubApiRef, DevAiHubClient } from './api/DevAiHubClient';\nimport { rootRouteRef } from './routes';\n\nexport const devAiHubPlugin = createFrontendPlugin({\n pluginId: 'dev-ai-hub',\n routes: {\n root: rootRouteRef,\n },\n extensions: [\n ApiBlueprint.make({\n params: defineParams =>\n defineParams(\n createApiFactory({\n api: devAiHubApiRef,\n deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },\n factory: ({ discoveryApi, fetchApi }) =>\n new DevAiHubClient(discoveryApi, fetchApi),\n }),\n ),\n }),\n PageBlueprint.make({\n params: {\n path: '/dev-ai-hub',\n routeRef: rootRouteRef,\n loader: () =>\n import('./components/DevAiHubPage').then(m => <m.DevAiHubPage />),\n },\n }),\n NavItemBlueprint.make({\n params: {\n title: 'AI Hub',\n routeRef: rootRouteRef,\n icon: HubIcon,\n },\n }),\n ],\n});\n"],"names":[],"mappings":";;;;;;AAaO,MAAM,iBAAiB,oBAAA,CAAqB;AAAA,EACjD,QAAA,EAAU,YAAA;AAAA,EACV,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA,GACR;AAAA,EACA,UAAA,EAAY;AAAA,IACV,aAAa,IAAA,CAAK;AAAA,MAChB,QAAQ,CAAA,YAAA,KACN,YAAA;AAAA,QACE,gBAAA,CAAiB;AAAA,UACf,GAAA,EAAK,cAAA;AAAA,UACL,IAAA,EAAM,EAAE,YAAA,EAAc,eAAA,EAAiB,UAAU,WAAA,EAAY;AAAA,UAC7D,OAAA,EAAS,CAAC,EAAE,YAAA,EAAc,UAAS,KACjC,IAAI,cAAA,CAAe,YAAA,EAAc,QAAQ;AAAA,SAC5C;AAAA;AACH,KACH,CAAA;AAAA,IACD,cAAc,IAAA,CAAK;AAAA,MACjB,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,aAAA;AAAA,QACN,QAAA,EAAU,YAAA;AAAA,QACV,MAAA,EAAQ,MACN,OAAO,wCAA2B,CAAA,CAAE,IAAA,CAAK,CAAA,CAAA,qBAAK,GAAA,CAAC,CAAA,CAAE,YAAA,EAAF,EAAe,CAAE;AAAA;AACpE,KACD,CAAA;AAAA,IACD,iBAAiB,IAAA,CAAK;AAAA,MACpB,MAAA,EAAQ;AAAA,QACN,KAAA,EAAO,QAAA;AAAA,QACP,QAAA,EAAU,YAAA;AAAA,QACV,IAAA,EAAM;AAAA;AACR,KACD;AAAA;AAEL,CAAC;;;;"}
@@ -0,0 +1,27 @@
1
+ import { createRouteRef, createPlugin, createApiFactory, fetchApiRef, discoveryApiRef, createRoutableExtension } from '@backstage/core-plugin-api';
2
+ import { DevAiHubClient, devAiHubApiRef } from './api/DevAiHubClient.esm.js';
3
+
4
+ const rootRouteRef = createRouteRef({ id: "dev-ai-hub" });
5
+ const devAiHubLegacyPlugin = createPlugin({
6
+ id: "dev-ai-hub",
7
+ routes: {
8
+ root: rootRouteRef
9
+ },
10
+ apis: [
11
+ createApiFactory({
12
+ api: devAiHubApiRef,
13
+ deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
14
+ factory: ({ discoveryApi, fetchApi }) => new DevAiHubClient(discoveryApi, fetchApi)
15
+ })
16
+ ]
17
+ });
18
+ const DevAiHubPage = devAiHubLegacyPlugin.provide(
19
+ createRoutableExtension({
20
+ name: "DevAiHubPage",
21
+ component: () => import('./components/DevAiHubPage/index.esm.js').then((m) => m.DevAiHubPage),
22
+ mountPoint: rootRouteRef
23
+ })
24
+ );
25
+
26
+ export { DevAiHubPage };
27
+ //# sourceMappingURL=pluginLegacy.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pluginLegacy.esm.js","sources":["../src/pluginLegacy.ts"],"sourcesContent":["/**\n * Legacy Backstage frontend system support.\n *\n * For New Frontend System (NFS) use `devAiHubPlugin` from `./plugin` instead.\n */\nimport type { ComponentType } from 'react';\nimport {\n createPlugin,\n createRoutableExtension,\n createRouteRef,\n createApiFactory,\n discoveryApiRef,\n fetchApiRef,\n} from '@backstage/core-plugin-api';\nimport { devAiHubApiRef, DevAiHubClient } from './api/DevAiHubClient';\n\nconst rootRouteRef = createRouteRef({ id: 'dev-ai-hub' });\n\nconst devAiHubLegacyPlugin = createPlugin({\n id: 'dev-ai-hub',\n routes: {\n root: rootRouteRef,\n },\n apis: [\n createApiFactory({\n api: devAiHubApiRef,\n deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },\n factory: ({ discoveryApi, fetchApi }) =>\n new DevAiHubClient(discoveryApi, fetchApi),\n }),\n ],\n});\n\n/** Routable page extension for the legacy frontend system. */\nexport const DevAiHubPage: ComponentType = devAiHubLegacyPlugin.provide(\n createRoutableExtension({\n name: 'DevAiHubPage',\n component: () =>\n import('./components/DevAiHubPage').then(m => m.DevAiHubPage),\n mountPoint: rootRouteRef,\n }),\n);\n"],"names":[],"mappings":";;;AAgBA,MAAM,YAAA,GAAe,cAAA,CAAe,EAAE,EAAA,EAAI,cAAc,CAAA;AAExD,MAAM,uBAAuB,YAAA,CAAa;AAAA,EACxC,EAAA,EAAI,YAAA;AAAA,EACJ,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA,GACR;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,gBAAA,CAAiB;AAAA,MACf,GAAA,EAAK,cAAA;AAAA,MACL,IAAA,EAAM,EAAE,YAAA,EAAc,eAAA,EAAiB,UAAU,WAAA,EAAY;AAAA,MAC7D,OAAA,EAAS,CAAC,EAAE,YAAA,EAAc,UAAS,KACjC,IAAI,cAAA,CAAe,YAAA,EAAc,QAAQ;AAAA,KAC5C;AAAA;AAEL,CAAC,CAAA;AAGM,MAAM,eAA8B,oBAAA,CAAqB,OAAA;AAAA,EAC9D,uBAAA,CAAwB;AAAA,IACtB,IAAA,EAAM,cAAA;AAAA,IACN,SAAA,EAAW,MACT,OAAO,wCAA2B,EAAE,IAAA,CAAK,CAAA,CAAA,KAAK,EAAE,YAAY,CAAA;AAAA,IAC9D,UAAA,EAAY;AAAA,GACb;AACH;;;;"}
@@ -0,0 +1,6 @@
1
+ import { createRouteRef } from '@backstage/frontend-plugin-api';
2
+
3
+ const rootRouteRef = createRouteRef();
4
+
5
+ export { rootRouteRef };
6
+ //# sourceMappingURL=routes.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.esm.js","sources":["../src/routes.ts"],"sourcesContent":["import { createRouteRef } from '@backstage/frontend-plugin-api';\n\nexport const rootRouteRef = createRouteRef();\n"],"names":[],"mappings":";;AAEO,MAAM,eAAe,cAAA;;;;"}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@nospt/plugin-dev-ai-hub",
3
+ "version": "0.1.0",
4
+ "description": "Frontend plugin for Dev AI Hub",
5
+ "license": "Apache-2.0",
6
+ "main": "./dist/index.esm.js",
7
+ "types": "./dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.esm.js",
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.esm.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "typesVersions": {
20
+ "*": {
21
+ "package.json": [
22
+ "package.json"
23
+ ]
24
+ }
25
+ },
26
+ "backstage": {
27
+ "role": "frontend-plugin",
28
+ "pluginId": "dev-ai-hub",
29
+ "pluginPackages": [
30
+ "@nospt/plugin-dev-ai-hub",
31
+ "@nospt/plugin-dev-ai-hub-backend",
32
+ "@nospt/plugin-dev-ai-hub-common",
33
+ "@nospt/plugin-dev-ai-hub-node"
34
+ ]
35
+ },
36
+ "sideEffects": false,
37
+ "scripts": {
38
+ "start": "backstage-cli package start --config ../../app-config.dev.yaml",
39
+ "build": "backstage-cli package build",
40
+ "clean": "backstage-cli package clean",
41
+ "lint": "backstage-cli package lint",
42
+ "prepack": "backstage-cli package prepack",
43
+ "postpack": "backstage-cli package postpack",
44
+ "test": "backstage-cli package test --passWithNoTests",
45
+ "tsc": "tsc --noEmit"
46
+ },
47
+ "dependencies": {
48
+ "@backstage/core-components": "*",
49
+ "@backstage/core-plugin-api": "*",
50
+ "@backstage/frontend-plugin-api": "*",
51
+ "@mui/icons-material": "^5.15.0",
52
+ "@mui/material": "^5.15.0",
53
+ "@nospt/plugin-dev-ai-hub-common": "*",
54
+ "react-markdown": "^9.0.0",
55
+ "react-syntax-highlighter": "^16.1.1",
56
+ "remark-gfm": "^4.0.1",
57
+ "simple-icons": "^13.0.0"
58
+ },
59
+ "devDependencies": {
60
+ "@backstage/cli": "*",
61
+ "@backstage/dev-utils": "*",
62
+ "@backstage/test-utils": "*",
63
+ "@testing-library/jest-dom": "^6.0.0",
64
+ "@testing-library/react": "^14.0.0",
65
+ "@types/jest": "^29.0.0",
66
+ "@types/react": "^18.0.0",
67
+ "@types/react-syntax-highlighter": "^15.0.0",
68
+ "jest": "^29.0.0",
69
+ "jest-environment-jsdom": "^29.0.0",
70
+ "react": "^18.0.0",
71
+ "react-dom": "^18.0.0",
72
+ "react-router-dom": "^6.0.0"
73
+ },
74
+ "peerDependencies": {
75
+ "react": "^18.0.0",
76
+ "react-dom": "^18.0.0",
77
+ "react-router-dom": "^6.0.0"
78
+ },
79
+ "module": "./dist/index.esm.js"
80
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@nospt/plugin-dev-ai-hub",
3
+ "version": "0.1.0",
4
+ "description": "Frontend plugin for Dev AI Hub",
5
+ "license": "Apache-2.0",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "exports": {
12
+ ".": "./src/index.ts",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "typesVersions": {
16
+ "*": {
17
+ "package.json": [
18
+ "package.json"
19
+ ]
20
+ }
21
+ },
22
+ "backstage": {
23
+ "role": "frontend-plugin",
24
+ "pluginId": "dev-ai-hub",
25
+ "pluginPackages": [
26
+ "@nospt/plugin-dev-ai-hub",
27
+ "@nospt/plugin-dev-ai-hub-backend",
28
+ "@nospt/plugin-dev-ai-hub-common",
29
+ "@nospt/plugin-dev-ai-hub-node"
30
+ ]
31
+ },
32
+ "sideEffects": false,
33
+ "scripts": {
34
+ "start": "backstage-cli package start --config ../../app-config.dev.yaml",
35
+ "build": "backstage-cli package build",
36
+ "clean": "backstage-cli package clean",
37
+ "lint": "backstage-cli package lint",
38
+ "prepack": "backstage-cli package prepack",
39
+ "postpack": "backstage-cli package postpack",
40
+ "test": "backstage-cli package test --passWithNoTests",
41
+ "tsc": "tsc --noEmit"
42
+ },
43
+ "dependencies": {
44
+ "@backstage/core-components": "*",
45
+ "@backstage/core-plugin-api": "*",
46
+ "@backstage/frontend-plugin-api": "*",
47
+ "@mui/icons-material": "^5.15.0",
48
+ "@mui/material": "^5.15.0",
49
+ "@nospt/plugin-dev-ai-hub-common": "*",
50
+ "react-markdown": "^9.0.0",
51
+ "react-syntax-highlighter": "^16.1.1",
52
+ "remark-gfm": "^4.0.1",
53
+ "simple-icons": "^13.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@backstage/cli": "*",
57
+ "@backstage/dev-utils": "*",
58
+ "@backstage/test-utils": "*",
59
+ "@testing-library/jest-dom": "^6.0.0",
60
+ "@testing-library/react": "^14.0.0",
61
+ "@types/jest": "^29.0.0",
62
+ "@types/react": "^18.0.0",
63
+ "@types/react-syntax-highlighter": "^15.0.0",
64
+ "jest": "^29.0.0",
65
+ "jest-environment-jsdom": "^29.0.0",
66
+ "react": "^18.0.0",
67
+ "react-dom": "^18.0.0",
68
+ "react-router-dom": "^6.0.0"
69
+ },
70
+ "peerDependencies": {
71
+ "react": "^18.0.0",
72
+ "react-dom": "^18.0.0",
73
+ "react-router-dom": "^6.0.0"
74
+ }
75
+ }
@@ -0,0 +1,110 @@
1
+ import {
2
+ createApiRef,
3
+ DiscoveryApi,
4
+ FetchApi,
5
+ } from '@backstage/core-plugin-api';
6
+ import type {
7
+ AiAsset,
8
+ AiAssetListResponse,
9
+ AiHubProvider,
10
+ AiHubStats,
11
+ AssetListFilter,
12
+ } from '@nospt/plugin-dev-ai-hub-common';
13
+
14
+ export const devAiHubApiRef = createApiRef<DevAiHubApi>({
15
+ id: 'plugin.dev-ai-hub.service',
16
+ });
17
+
18
+ export interface DevAiHubApi {
19
+ listAssets(filter?: AssetListFilter): Promise<AiAssetListResponse>;
20
+ getAsset(id: string): Promise<AiAsset>;
21
+ getAssetRaw(id: string): Promise<string>;
22
+ /** Returns the absolute URL for the download endpoint (zip for skills, md for others). */
23
+ getDownloadUrl(id: string): Promise<string>;
24
+ trackInstall(id: string): Promise<void>;
25
+ listProviders(): Promise<AiHubProvider[]>;
26
+ getProviderStatus(id: string): Promise<AiHubProvider>;
27
+ triggerSync(id: string): Promise<void>;
28
+ getStats(): Promise<AiHubStats>;
29
+ }
30
+
31
+ export class DevAiHubClient implements DevAiHubApi {
32
+ constructor(
33
+ private readonly discoveryApi: DiscoveryApi,
34
+ private readonly fetchApi: FetchApi,
35
+ ) {}
36
+
37
+ private async baseUrl(): Promise<string> {
38
+ return this.discoveryApi.getBaseUrl('dev-ai-hub');
39
+ }
40
+
41
+ private async fetch<T>(path: string, init?: RequestInit): Promise<T> {
42
+ const base = await this.baseUrl();
43
+ const response = await this.fetchApi.fetch(`${base}${path}`, init);
44
+ if (!response.ok) {
45
+ const text = await response.text();
46
+ throw new Error(
47
+ `Dev AI Hub API error ${response.status}: ${text}`,
48
+ );
49
+ }
50
+ return response.json() as Promise<T>;
51
+ }
52
+
53
+ async listAssets(filter?: AssetListFilter): Promise<AiAssetListResponse> {
54
+ const params = new URLSearchParams();
55
+ if (filter?.type) params.set('type', filter.type);
56
+ if (filter?.tool) params.set('tool', filter.tool);
57
+ if (filter?.search) params.set('search', filter.search);
58
+ if (filter?.providerId) params.set('provider', filter.providerId);
59
+ if (filter?.tags?.length) params.set('tags', filter.tags.join(','));
60
+ if (filter?.page) params.set('page', String(filter.page));
61
+ if (filter?.pageSize) params.set('pageSize', String(filter.pageSize));
62
+
63
+ const qs = params.toString();
64
+ return this.fetch<AiAssetListResponse>(`/assets${qs ? `?${qs}` : ''}`);
65
+ }
66
+
67
+ async getAsset(id: string): Promise<AiAsset> {
68
+ return this.fetch<AiAsset>(`/assets/${encodeURIComponent(id)}`);
69
+ }
70
+
71
+ async getAssetRaw(id: string): Promise<string> {
72
+ const base = await this.baseUrl();
73
+ const response = await this.fetchApi.fetch(
74
+ `${base}/assets/${encodeURIComponent(id)}/raw`,
75
+ );
76
+ if (!response.ok) throw new Error(`Failed to fetch raw asset: ${response.status}`);
77
+ return response.text();
78
+ }
79
+
80
+ async getDownloadUrl(id: string): Promise<string> {
81
+ const base = await this.baseUrl();
82
+ return `${base}/assets/${encodeURIComponent(id)}/download`;
83
+ }
84
+
85
+ async trackInstall(id: string): Promise<void> {
86
+ await this.fetch(`/assets/${encodeURIComponent(id)}/track-install`, {
87
+ method: 'POST',
88
+ });
89
+ }
90
+
91
+ async listProviders(): Promise<AiHubProvider[]> {
92
+ return this.fetch<AiHubProvider[]>('/providers');
93
+ }
94
+
95
+ async getProviderStatus(id: string): Promise<AiHubProvider> {
96
+ return this.fetch<AiHubProvider>(
97
+ `/providers/${encodeURIComponent(id)}/status`,
98
+ );
99
+ }
100
+
101
+ async triggerSync(id: string): Promise<void> {
102
+ await this.fetch(`/providers/${encodeURIComponent(id)}/sync`, {
103
+ method: 'POST',
104
+ });
105
+ }
106
+
107
+ async getStats(): Promise<AiHubStats> {
108
+ return this.fetch<AiHubStats>('/stats');
109
+ }
110
+ }
@@ -0,0 +1,182 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { AssetCard } from './AssetCard';
4
+ import type { AiAssetSummary } from '@nospt/plugin-dev-ai-hub-common';
5
+
6
+ jest.mock('../ToolIcon', () => ({
7
+ ToolIcon: ({ tool }: { tool: string }) => <span data-testid={`tool-icon-${tool}`} />,
8
+ }));
9
+
10
+ const NOW = new Date('2026-03-14T12:00:00Z').getTime();
11
+
12
+ function makeAsset(overrides?: Partial<AiAssetSummary>): AiAssetSummary {
13
+ return {
14
+ id: 'asset-1',
15
+ providerId: 'provider-1',
16
+ name: 'My Instruction',
17
+ description: 'A helpful coding instruction',
18
+ type: 'instruction',
19
+ tools: ['claude-code'],
20
+ tags: ['python', 'linting'],
21
+ author: 'Test Author',
22
+ version: '1.0.0',
23
+ installCount: 0,
24
+ syncedAt: '2026-03-14T12:00:00Z',
25
+ createdAt: '2026-03-01T00:00:00Z',
26
+ updatedAt: '2026-03-13T00:00:00Z', // 1 day ago — "New"
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ describe('AssetCard', () => {
32
+ beforeEach(() => {
33
+ jest.spyOn(Date, 'now').mockReturnValue(NOW);
34
+ });
35
+
36
+ afterEach(() => {
37
+ jest.restoreAllMocks();
38
+ });
39
+
40
+ it('renders asset name and description', () => {
41
+ render(<AssetCard asset={makeAsset()} onView={jest.fn()} onInstall={jest.fn()} />);
42
+ expect(screen.getByText('My Instruction')).toBeInTheDocument();
43
+ expect(screen.getByText('A helpful coding instruction')).toBeInTheDocument();
44
+ });
45
+
46
+ it('renders the type label', () => {
47
+ render(<AssetCard asset={makeAsset({ type: 'instruction' })} onView={jest.fn()} onInstall={jest.fn()} />);
48
+ expect(screen.getByText('Instruction')).toBeInTheDocument();
49
+ });
50
+
51
+ it('renders the correct type label for each type', () => {
52
+ const types = ['agent', 'skill', 'workflow'] as const;
53
+ const labels = ['Agent', 'Skill', 'Workflow'];
54
+
55
+ types.forEach((type, i) => {
56
+ const { unmount } = render(
57
+ <AssetCard asset={makeAsset({ type })} onView={jest.fn()} onInstall={jest.fn()} />,
58
+ );
59
+ expect(screen.getByText(labels[i])).toBeInTheDocument();
60
+ unmount();
61
+ });
62
+ });
63
+
64
+ it('renders author and version in the footer', () => {
65
+ render(<AssetCard asset={makeAsset()} onView={jest.fn()} onInstall={jest.fn()} />);
66
+ expect(screen.getByText(/v1\.0\.0/)).toBeInTheDocument();
67
+ expect(screen.getByText(/Test Author/)).toBeInTheDocument();
68
+ });
69
+
70
+ describe('"New" badge', () => {
71
+ it('shows "New" badge when updatedAt is within 14 days', () => {
72
+ const recentDate = new Date(NOW - 5 * 24 * 60 * 60 * 1000).toISOString(); // 5 days ago
73
+ render(
74
+ <AssetCard asset={makeAsset({ updatedAt: recentDate })} onView={jest.fn()} onInstall={jest.fn()} />,
75
+ );
76
+ expect(screen.getByText('New')).toBeInTheDocument();
77
+ });
78
+
79
+ it('does not show "New" badge when updatedAt is older than 14 days', () => {
80
+ const oldDate = new Date(NOW - 20 * 24 * 60 * 60 * 1000).toISOString(); // 20 days ago
81
+ render(
82
+ <AssetCard asset={makeAsset({ updatedAt: oldDate })} onView={jest.fn()} onInstall={jest.fn()} />,
83
+ );
84
+ expect(screen.queryByText('New')).not.toBeInTheDocument();
85
+ });
86
+ });
87
+
88
+ describe('install count display', () => {
89
+ it('does not show install count when installCount is 0', () => {
90
+ render(<AssetCard asset={makeAsset({ installCount: 0 })} onView={jest.fn()} onInstall={jest.fn()} />);
91
+ expect(screen.queryByText('↓')).not.toBeInTheDocument();
92
+ expect(screen.queryByText('🔥')).not.toBeInTheDocument();
93
+ });
94
+
95
+ it('shows "↓" arrow with count when installCount > 0 and < 5', () => {
96
+ render(<AssetCard asset={makeAsset({ installCount: 3 })} onView={jest.fn()} onInstall={jest.fn()} />);
97
+ expect(screen.getByText('↓')).toBeInTheDocument();
98
+ expect(screen.getByText('3')).toBeInTheDocument();
99
+ });
100
+
101
+ it('shows 🔥 emoji when installCount >= 5 (popular threshold)', () => {
102
+ render(<AssetCard asset={makeAsset({ installCount: 5 })} onView={jest.fn()} onInstall={jest.fn()} />);
103
+ expect(screen.getByText('🔥')).toBeInTheDocument();
104
+ expect(screen.getByText('5')).toBeInTheDocument();
105
+ });
106
+
107
+ it('shows 🔥 emoji for installCount > 5', () => {
108
+ render(<AssetCard asset={makeAsset({ installCount: 42 })} onView={jest.fn()} onInstall={jest.fn()} />);
109
+ expect(screen.getByText('🔥')).toBeInTheDocument();
110
+ expect(screen.queryByText('↓')).not.toBeInTheDocument();
111
+ });
112
+ });
113
+
114
+ describe('callbacks', () => {
115
+ it('calls onView with the asset id when "View details" button is clicked', () => {
116
+ const onView = jest.fn();
117
+ render(<AssetCard asset={makeAsset()} onView={onView} onInstall={jest.fn()} />);
118
+ fireEvent.click(screen.getByRole('button', { name: 'View details' }));
119
+ expect(onView).toHaveBeenCalledTimes(1);
120
+ expect(onView).toHaveBeenCalledWith('asset-1');
121
+ });
122
+
123
+ it('calls onInstall with the asset id when "Install in editor" button is clicked', () => {
124
+ const onInstall = jest.fn();
125
+ render(<AssetCard asset={makeAsset()} onView={jest.fn()} onInstall={onInstall} />);
126
+ fireEvent.click(screen.getByRole('button', { name: 'Install in editor' }));
127
+ expect(onInstall).toHaveBeenCalledTimes(1);
128
+ expect(onInstall).toHaveBeenCalledWith('asset-1');
129
+ });
130
+ });
131
+
132
+ describe('tags', () => {
133
+ it('renders tags as chips prefixed with #', () => {
134
+ render(
135
+ <AssetCard asset={makeAsset({ tags: ['python', 'linting'] })} onView={jest.fn()} onInstall={jest.fn()} />,
136
+ );
137
+ expect(screen.getByText('#python')).toBeInTheDocument();
138
+ expect(screen.getByText('#linting')).toBeInTheDocument();
139
+ });
140
+
141
+ it('renders at most 3 tags and shows "+N" for the rest', () => {
142
+ render(
143
+ <AssetCard
144
+ asset={makeAsset({ tags: ['a', 'b', 'c', 'd', 'e'] })}
145
+ onView={jest.fn()}
146
+ onInstall={jest.fn()}
147
+ />,
148
+ );
149
+ expect(screen.getByText('#a')).toBeInTheDocument();
150
+ expect(screen.getByText('#b')).toBeInTheDocument();
151
+ expect(screen.getByText('#c')).toBeInTheDocument();
152
+ expect(screen.queryByText('#d')).not.toBeInTheDocument();
153
+ expect(screen.getByText('+2')).toBeInTheDocument();
154
+ });
155
+
156
+ it('renders no tags section when tags is empty', () => {
157
+ render(<AssetCard asset={makeAsset({ tags: [] })} onView={jest.fn()} onInstall={jest.fn()} />);
158
+ expect(screen.queryByText(/#/)).not.toBeInTheDocument();
159
+ });
160
+ });
161
+
162
+ describe('tools', () => {
163
+ it('renders a tool icon for each tool', () => {
164
+ render(
165
+ <AssetCard
166
+ asset={makeAsset({ tools: ['claude-code', 'github-copilot'] })}
167
+ onView={jest.fn()}
168
+ onInstall={jest.fn()}
169
+ />,
170
+ );
171
+ expect(screen.getByTestId('tool-icon-claude-code')).toBeInTheDocument();
172
+ expect(screen.getByTestId('tool-icon-github-copilot')).toBeInTheDocument();
173
+ });
174
+
175
+ it('shows "Universal" label for the "all" tool', () => {
176
+ render(
177
+ <AssetCard asset={makeAsset({ tools: ['all'] })} onView={jest.fn()} onInstall={jest.fn()} />,
178
+ );
179
+ expect(screen.getByText('Universal')).toBeInTheDocument();
180
+ });
181
+ });
182
+ });