@forgeportal/plugin-sdk 1.0.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.
@@ -0,0 +1,4 @@
1
+
2
+ > @forgeportal/plugin-sdk@1.0.0 build /home/runner/work/forgeportal/forgeportal/packages/plugin-sdk
3
+ > tsc
4
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bendaamerahmed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # @forgeportal/plugin-sdk
2
+
3
+ The official SDK for building ForgePortal plugins.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @forgeportal/plugin-sdk
9
+ # For UI plugins also install peer deps:
10
+ pnpm add react @tanstack/react-query
11
+ ```
12
+
13
+ ## Plugin Types
14
+
15
+ | Type | What it provides |
16
+ |-------------|-----------------------------------------------------|
17
+ | `ui` | Entity tabs, entity cards, top-level routes |
18
+ | `backend` | Fastify routes, action providers, catalog providers |
19
+ | `fullstack` | Both UI and backend capabilities |
20
+
21
+ ## Quick Start — UI Plugin
22
+
23
+ ```typescript
24
+ // src/index.ts
25
+ import type { ForgePluginSDK } from '@forgeportal/plugin-sdk';
26
+ import { MyEntityTab } from './MyEntityTab.js';
27
+
28
+ export function registerPlugin(sdk: ForgePluginSDK) {
29
+ sdk.registerEntityTab({
30
+ id: 'my-plugin-tab',
31
+ title: 'My Plugin',
32
+ component: MyEntityTab,
33
+ appliesTo: { kinds: ['service'] },
34
+ });
35
+ }
36
+ ```
37
+
38
+ ```typescript
39
+ // src/MyEntityTab.tsx
40
+ import { useEntity } from '@forgeportal/plugin-sdk/react';
41
+
42
+ export function MyEntityTab() {
43
+ const { entity } = useEntity();
44
+ return <div>Entity: {entity.name}</div>;
45
+ }
46
+ ```
47
+
48
+ ## Quick Start — Backend Plugin (Action Provider)
49
+
50
+ ```typescript
51
+ // src/actions/myAction.ts
52
+ import type { ActionProvider } from '@forgeportal/plugin-sdk';
53
+
54
+ export const myAction: ActionProvider = {
55
+ id: 'myplugin.doSomething',
56
+ version: 'v1',
57
+ schema: {
58
+ input: {
59
+ type: 'object',
60
+ properties: { message: { type: 'string', title: 'Message' } },
61
+ required: ['message'],
62
+ },
63
+ },
64
+ async handler(ctx, input) {
65
+ ctx.logger.info('Running action', { input });
66
+ await ctx.log('info', `Processing: ${input['message']}`);
67
+ return { status: 'success', outputs: { done: true } };
68
+ },
69
+ };
70
+ ```
71
+
72
+ ## Quick Start — Fullstack Plugin (Catalog Provider)
73
+
74
+ ```typescript
75
+ // src/index.ts
76
+ import type { ForgePluginSDK } from '@forgeportal/plugin-sdk';
77
+ import { myAction } from './actions/myAction.js';
78
+ import { MyCatalogTab } from './MyCatalogTab.js';
79
+ import { myCatalogProvider } from './catalog.js';
80
+
81
+ export function registerPlugin(sdk: ForgePluginSDK) {
82
+ // Backend: action provider
83
+ sdk.registerActionProvider(myAction);
84
+ // Backend: catalog ingest provider
85
+ sdk.registerCatalogProvider(myCatalogProvider);
86
+ // UI: custom entity tab
87
+ sdk.registerEntityTab({
88
+ id: 'my-catalog-tab',
89
+ title: 'My Catalog',
90
+ component: MyCatalogTab,
91
+ });
92
+ }
93
+ ```
94
+
95
+ ## React Hooks
96
+
97
+ | Hook | Description |
98
+ |-------------------|------------------------------------------------------------------------|
99
+ | `useEntity()` | Returns `{ entity }` — the current catalog entity. Use inside EntityTab/EntityCard. |
100
+ | `useConfig<T>(key)` | Returns plugin config value from `forgeportal.yaml`. |
101
+ | `useApi<T>(path)` | TanStack Query wrapper for ForgePortal API calls. |
102
+
103
+ ```typescript
104
+ import { useEntity, useConfig, useApi } from '@forgeportal/plugin-sdk/react';
105
+
106
+ function MyTab() {
107
+ const { entity } = useEntity();
108
+ const apiUrl = useConfig<string>('apiEndpoint');
109
+ const { data } = useApi<{ incidents: unknown[] }>(`/api/v1/my-plugin/${entity.id}/incidents`);
110
+ return <pre>{JSON.stringify(data, null, 2)}</pre>;
111
+ }
112
+ ```
113
+
114
+ ## Plugin Manifest (`forgeportal-plugin.json`)
115
+
116
+ ```json
117
+ {
118
+ "name": "@myorg/forge-plugin-my-plugin",
119
+ "version": "1.0.0",
120
+ "forgeportal": {
121
+ "engineVersion": "^1.0.0",
122
+ "type": "ui",
123
+ "capabilities": {
124
+ "ui": {
125
+ "entityTabs": ["my-plugin-tab"]
126
+ }
127
+ },
128
+ "config": {
129
+ "apiEndpoint": {
130
+ "type": "string",
131
+ "description": "External service URL",
132
+ "required": true
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## ActionContext Services
140
+
141
+ | Property | Type | Description |
142
+ |--------------------|-------------------------|-----------------------------------------------------|
143
+ | `config` | `ActionConfigAccessor` | Plugin config from `forgeportal.yaml` |
144
+ | `logger` | `ActionLogger` | Structured logger (info/warn/error) |
145
+ | `scm` | `ActionScmAccessor` | SCM file reads (getFile, listFiles) |
146
+ | `db` | `ActionDbAccessor` | Read-only SQL query access |
147
+ | `acquireRepoLock` | `(repoUrl) => Promise` | Advisory lock to prevent concurrent SCM writes |
148
+ | `log` | `(level, msg) => Promise` | Persisted action run log line |
149
+
150
+ ## Versioning
151
+
152
+ The SDK follows semantic versioning:
153
+
154
+ - **Patch** — bug fixes, no interface changes
155
+ - **Minor** — new capabilities (backward-compatible)
156
+ - **Major** — breaking contract changes
157
+
158
+ Plugins declare `engineVersion: "^1.0.0"` to stay compatible with any 1.x SDK release.
159
+
160
+ The current SDK version is exported as `SDK_VERSION`:
161
+
162
+ ```typescript
163
+ import { SDK_VERSION } from '@forgeportal/plugin-sdk';
164
+ console.log(SDK_VERSION); // "1.0.0"
165
+ ```
@@ -0,0 +1,195 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { PluginRegistry } from '../src/registry.js';
3
+ import type { EntityTab, EntityCard, Route, ActionProvider, CatalogProvider } from '../src/types.js';
4
+
5
+ // ─── Test fixtures ─────────────────────────────────────────────────────────
6
+
7
+ function makeTab(id: string, kinds?: string[]): EntityTab {
8
+ return {
9
+ id,
10
+ title: `Tab ${id}`,
11
+ component: () => null,
12
+ appliesTo: kinds ? { kinds } : undefined,
13
+ };
14
+ }
15
+
16
+ function makeCard(id: string, kinds?: string[]): EntityCard {
17
+ return {
18
+ id,
19
+ title: `Card ${id}`,
20
+ component: () => null,
21
+ appliesTo: kinds ? { kinds } : undefined,
22
+ };
23
+ }
24
+
25
+ function makeRoute(path: string, navLabel?: string): Route {
26
+ return { path, component: () => null, navLabel };
27
+ }
28
+
29
+ function makeActionProvider(id: string, version = 'v1'): ActionProvider {
30
+ return {
31
+ id,
32
+ version,
33
+ schema: { input: { type: 'object', properties: {} } },
34
+ handler: vi.fn().mockResolvedValue({ status: 'success', outputs: {} }),
35
+ };
36
+ }
37
+
38
+ function makeCatalogProvider(id: string): CatalogProvider {
39
+ return {
40
+ id,
41
+ async *ingest() { /* yields nothing */ },
42
+ };
43
+ }
44
+
45
+ // ─── EntityTab tests ────────────────────────────────────────────────────────
46
+
47
+ describe('PluginRegistry — EntityTab', () => {
48
+ it('registers a tab and retrieves it', () => {
49
+ const reg = new PluginRegistry();
50
+ reg.registerEntityTab(makeTab('tab-a'));
51
+ expect(reg.getEntityTabs()).toHaveLength(1);
52
+ expect(reg.getEntityTabs()[0]?.id).toBe('tab-a');
53
+ });
54
+
55
+ it('skips duplicate registration (warns)', () => {
56
+ const reg = new PluginRegistry();
57
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
58
+ reg.registerEntityTab(makeTab('tab-dup'));
59
+ reg.registerEntityTab(makeTab('tab-dup'));
60
+ expect(reg.getEntityTabs()).toHaveLength(1);
61
+ expect(warnSpy).toHaveBeenCalledOnce();
62
+ warnSpy.mockRestore();
63
+ });
64
+
65
+ it('filters tabs by entity kind — matching kind', () => {
66
+ const reg = new PluginRegistry();
67
+ reg.registerEntityTab(makeTab('tab-svc', ['service']));
68
+ reg.registerEntityTab(makeTab('tab-all')); // no kinds = all
69
+ const tabs = reg.getEntityTabs('service');
70
+ expect(tabs.map(t => t.id)).toEqual(['tab-svc', 'tab-all']);
71
+ });
72
+
73
+ it('filters tabs by entity kind — non-matching kind', () => {
74
+ const reg = new PluginRegistry();
75
+ reg.registerEntityTab(makeTab('tab-svc', ['service']));
76
+ reg.registerEntityTab(makeTab('tab-all'));
77
+ const tabs = reg.getEntityTabs('library');
78
+ expect(tabs.map(t => t.id)).toEqual(['tab-all']);
79
+ });
80
+
81
+ it('returns all tabs when no kind filter provided', () => {
82
+ const reg = new PluginRegistry();
83
+ reg.registerEntityTab(makeTab('a', ['service']));
84
+ reg.registerEntityTab(makeTab('b', ['library']));
85
+ expect(reg.getEntityTabs()).toHaveLength(2);
86
+ });
87
+ });
88
+
89
+ // ─── EntityCard tests ───────────────────────────────────────────────────────
90
+
91
+ describe('PluginRegistry — EntityCard', () => {
92
+ it('registers and retrieves a card', () => {
93
+ const reg = new PluginRegistry();
94
+ reg.registerEntityCard(makeCard('card-a'));
95
+ expect(reg.getEntityCards()).toHaveLength(1);
96
+ });
97
+
98
+ it('skips duplicate cards', () => {
99
+ const reg = new PluginRegistry();
100
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
101
+ reg.registerEntityCard(makeCard('card-dup'));
102
+ reg.registerEntityCard(makeCard('card-dup'));
103
+ expect(reg.getEntityCards()).toHaveLength(1);
104
+ warnSpy.mockRestore();
105
+ });
106
+
107
+ it('filters cards by entity kind', () => {
108
+ const reg = new PluginRegistry();
109
+ reg.registerEntityCard(makeCard('c-svc', ['service']));
110
+ reg.registerEntityCard(makeCard('c-all'));
111
+ expect(reg.getEntityCards('library').map(c => c.id)).toEqual(['c-all']);
112
+ expect(reg.getEntityCards('service').map(c => c.id)).toEqual(['c-svc', 'c-all']);
113
+ });
114
+ });
115
+
116
+ // ─── Route tests ────────────────────────────────────────────────────────────
117
+
118
+ describe('PluginRegistry — Route', () => {
119
+ it('registers routes and retrieves them', () => {
120
+ const reg = new PluginRegistry();
121
+ reg.registerRoute(makeRoute('/pd', 'PagerDuty'));
122
+ reg.registerRoute(makeRoute('/slack'));
123
+ const routes = reg.getRoutes();
124
+ expect(routes).toHaveLength(2);
125
+ expect(routes[0]?.navLabel).toBe('PagerDuty');
126
+ expect(routes[1]?.navLabel).toBeUndefined();
127
+ });
128
+
129
+ it('skips duplicate routes by path', () => {
130
+ const reg = new PluginRegistry();
131
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
132
+ reg.registerRoute(makeRoute('/pd'));
133
+ reg.registerRoute(makeRoute('/pd'));
134
+ expect(reg.getRoutes()).toHaveLength(1);
135
+ warnSpy.mockRestore();
136
+ });
137
+ });
138
+
139
+ // ─── ActionProvider tests ────────────────────────────────────────────────────
140
+
141
+ describe('PluginRegistry — ActionProvider', () => {
142
+ it('registers action providers by id@version key', () => {
143
+ const reg = new PluginRegistry();
144
+ reg.registerActionProvider(makeActionProvider('slack.send', 'v1'));
145
+ reg.registerActionProvider(makeActionProvider('slack.send', 'v2'));
146
+ expect(reg.getActionProviders()).toHaveLength(2);
147
+ });
148
+
149
+ it('retrieves a specific action provider by id and version', () => {
150
+ const reg = new PluginRegistry();
151
+ const provider = makeActionProvider('pd.create', 'v1');
152
+ reg.registerActionProvider(provider);
153
+ expect(reg.getActionProvider('pd.create', 'v1')).toBe(provider);
154
+ expect(reg.getActionProvider('pd.create', 'v2')).toBeUndefined();
155
+ });
156
+
157
+ it('skips duplicate id@version', () => {
158
+ const reg = new PluginRegistry();
159
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
160
+ reg.registerActionProvider(makeActionProvider('x', 'v1'));
161
+ reg.registerActionProvider(makeActionProvider('x', 'v1'));
162
+ expect(reg.getActionProviders()).toHaveLength(1);
163
+ warnSpy.mockRestore();
164
+ });
165
+ });
166
+
167
+ // ─── CatalogProvider tests ────────────────────────────────────────────────────
168
+
169
+ describe('PluginRegistry — CatalogProvider', () => {
170
+ it('registers catalog providers', () => {
171
+ const reg = new PluginRegistry();
172
+ reg.registerCatalogProvider(makeCatalogProvider('pd-catalog'));
173
+ expect(reg.getCatalogProviders()).toHaveLength(1);
174
+ });
175
+
176
+ it('skips duplicate catalog provider IDs', () => {
177
+ const reg = new PluginRegistry();
178
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
179
+ reg.registerCatalogProvider(makeCatalogProvider('dup'));
180
+ reg.registerCatalogProvider(makeCatalogProvider('dup'));
181
+ expect(reg.getCatalogProviders()).toHaveLength(1);
182
+ warnSpy.mockRestore();
183
+ });
184
+ });
185
+
186
+ // ─── Registry isolation ───────────────────────────────────────────────────────
187
+
188
+ describe('PluginRegistry — isolation', () => {
189
+ it('two registry instances do not share state', () => {
190
+ const a = new PluginRegistry();
191
+ const b = new PluginRegistry();
192
+ a.registerEntityTab(makeTab('shared-tab'));
193
+ expect(b.getEntityTabs()).toHaveLength(0);
194
+ });
195
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, it, expectTypeOf } from 'vitest';
2
+ import type {
3
+ ForgePluginSDK,
4
+ ActionProvider,
5
+ ActionContext,
6
+ ActionResult,
7
+ PluginManifest,
8
+ } from '../src/types.js';
9
+
10
+ describe('ForgePluginSDK interface', () => {
11
+ it('has all registration methods', () => {
12
+ type Methods = keyof ForgePluginSDK;
13
+ expectTypeOf<'registerEntityTab'>().toMatchTypeOf<Methods>();
14
+ expectTypeOf<'registerEntityCard'>().toMatchTypeOf<Methods>();
15
+ expectTypeOf<'registerRoute'>().toMatchTypeOf<Methods>();
16
+ expectTypeOf<'registerActionProvider'>().toMatchTypeOf<Methods>();
17
+ expectTypeOf<'registerCatalogProvider'>().toMatchTypeOf<Methods>();
18
+ });
19
+ });
20
+
21
+ describe('ActionResult interface', () => {
22
+ it('requires status and outputs', () => {
23
+ type Keys = keyof ActionResult;
24
+ expectTypeOf<'status'>().toMatchTypeOf<Keys>();
25
+ expectTypeOf<'outputs'>().toMatchTypeOf<Keys>();
26
+ });
27
+
28
+ it('status is a union of success | failed', () => {
29
+ expectTypeOf<ActionResult['status']>().toEqualTypeOf<'success' | 'failed'>();
30
+ });
31
+ });
32
+
33
+ describe('ActionContext interface', () => {
34
+ it('exposes all required service accessors', () => {
35
+ type Keys = keyof ActionContext;
36
+ expectTypeOf<'config'>().toMatchTypeOf<Keys>();
37
+ expectTypeOf<'logger'>().toMatchTypeOf<Keys>();
38
+ expectTypeOf<'scm'>().toMatchTypeOf<Keys>();
39
+ expectTypeOf<'db'>().toMatchTypeOf<Keys>();
40
+ expectTypeOf<'acquireRepoLock'>().toMatchTypeOf<Keys>();
41
+ expectTypeOf<'log'>().toMatchTypeOf<Keys>();
42
+ });
43
+
44
+ it('acquireRepoLock takes a string and returns Promise<void>', () => {
45
+ expectTypeOf<ActionContext['acquireRepoLock']>().toEqualTypeOf<(repoUrl: string) => Promise<void>>();
46
+ });
47
+ });
48
+
49
+ describe('PluginManifest interface', () => {
50
+ it('forgeportal.type is a union of plugin types', () => {
51
+ expectTypeOf<PluginManifest['forgeportal']['type']>().toEqualTypeOf<'ui' | 'backend' | 'fullstack'>();
52
+ });
53
+ });
54
+
55
+ describe('ActionProvider interface', () => {
56
+ it('handler returns Promise<ActionResult>', () => {
57
+ type HandlerReturn = ReturnType<ActionProvider['handler']>;
58
+ expectTypeOf<HandlerReturn>().toEqualTypeOf<Promise<ActionResult>>();
59
+ });
60
+ });
@@ -0,0 +1,56 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { ActionProvider, CatalogProvider, ActionConfigAccessor, ActionLogger } from './types.js';
3
+ /**
4
+ * A backend route registration — Fastify plugin function scoped under
5
+ * /api/v1/plugins/{pluginId}/{path}/
6
+ */
7
+ export interface BackendRoute {
8
+ /** Relative path prefix, e.g. '/alerts'. No leading slash required. */
9
+ path: string;
10
+ /**
11
+ * Fastify async plugin that registers route handlers.
12
+ * Receives a Fastify instance pre-scoped to the plugin's route prefix.
13
+ * Do NOT call fastify.listen() — only register routes.
14
+ */
15
+ handler: (fastify: FastifyInstance) => Promise<void>;
16
+ }
17
+ /**
18
+ * The SDK object passed to backend and fullstack plugin entry points.
19
+ * Distinct from ForgePluginSDK (which uses React components).
20
+ *
21
+ * Backend plugin entry: export function registerBackendPlugin(sdk: ForgeBackendPluginSDK): void
22
+ */
23
+ export interface ForgeBackendPluginSDK {
24
+ /** Plugin-scoped config accessor (from forgeportal.yaml plugins.<id>.config). */
25
+ readonly config: ActionConfigAccessor;
26
+ /** Structured logger scoped to this plugin. */
27
+ readonly logger: ActionLogger;
28
+ /** Register an action provider (available in action runner + templates). */
29
+ registerActionProvider(provider: ActionProvider): void;
30
+ /** Register a catalog provider (periodic ingestion of external entities). */
31
+ registerCatalogProvider(provider: CatalogProvider): void;
32
+ /**
33
+ * Register backend routes under /api/v1/plugins/{pluginId}/{route.path}/
34
+ * The Fastify instance passed to handler is already authenticated (authGuard runs).
35
+ */
36
+ registerBackendRoute(route: BackendRoute): void;
37
+ }
38
+ /**
39
+ * Backend plugin registry — in-memory store for backend capabilities.
40
+ * Instantiated by the plugin loader for each plugin.
41
+ */
42
+ export declare class BackendPluginRegistry implements ForgeBackendPluginSDK {
43
+ readonly config: ActionConfigAccessor;
44
+ readonly logger: ActionLogger;
45
+ private readonly _actionProviders;
46
+ private readonly _catalogProviders;
47
+ private readonly _routes;
48
+ constructor(config: ActionConfigAccessor, logger: ActionLogger);
49
+ registerActionProvider(provider: ActionProvider): void;
50
+ registerCatalogProvider(provider: CatalogProvider): void;
51
+ registerBackendRoute(route: BackendRoute): void;
52
+ getActionProviders(): ActionProvider[];
53
+ getCatalogProviders(): CatalogProvider[];
54
+ getBackendRoutes(): BackendRoute[];
55
+ }
56
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,IAAI,EAAK,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,4EAA4E;IAC5E,sBAAsB,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IACvD,6EAA6E;IAC7E,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACzD;;;OAGG;IACH,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACjD;AAED;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,qBAAqB;IAM/D,QAAQ,CAAC,MAAM,EAAE,oBAAoB;IACrC,QAAQ,CAAC,MAAM,EAAE,YAAY;IAN/B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsC;IACvE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAsC;IACxE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;gBAGnC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,YAAY;IAG/B,sBAAsB,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI;IAStD,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAQxD,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAI/C,kBAAkB,IAAK,cAAc,EAAE;IACvC,mBAAmB,IAAI,eAAe,EAAE;IACxC,gBAAgB,IAAO,YAAY,EAAE;CACtC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Backend plugin registry — in-memory store for backend capabilities.
3
+ * Instantiated by the plugin loader for each plugin.
4
+ */
5
+ export class BackendPluginRegistry {
6
+ config;
7
+ logger;
8
+ _actionProviders = new Map();
9
+ _catalogProviders = new Map();
10
+ _routes = [];
11
+ constructor(config, logger) {
12
+ this.config = config;
13
+ this.logger = logger;
14
+ }
15
+ registerActionProvider(provider) {
16
+ const key = `${provider.id}@${provider.version}`;
17
+ if (this._actionProviders.has(key)) {
18
+ console.warn(`[ForgePortal SDK] ActionProvider "${key}" already registered — skipping.`);
19
+ return;
20
+ }
21
+ this._actionProviders.set(key, provider);
22
+ }
23
+ registerCatalogProvider(provider) {
24
+ if (this._catalogProviders.has(provider.id)) {
25
+ console.warn(`[ForgePortal SDK] CatalogProvider "${provider.id}" already registered — skipping.`);
26
+ return;
27
+ }
28
+ this._catalogProviders.set(provider.id, provider);
29
+ }
30
+ registerBackendRoute(route) {
31
+ this._routes.push(route);
32
+ }
33
+ getActionProviders() { return [...this._actionProviders.values()]; }
34
+ getCatalogProviders() { return [...this._catalogProviders.values()]; }
35
+ getBackendRoutes() { return [...this._routes]; }
36
+ }
37
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AA6CA;;;GAGG;AACH,MAAM,OAAO,qBAAqB;IAMrB;IACA;IANM,gBAAgB,GAAI,IAAI,GAAG,EAA0B,CAAC;IACtD,iBAAiB,GAAG,IAAI,GAAG,EAA2B,CAAC;IACvD,OAAO,GAAmB,EAAE,CAAC;IAE9C,YACW,MAA4B,EAC5B,MAAoB;QADpB,WAAM,GAAN,MAAM,CAAsB;QAC5B,WAAM,GAAN,MAAM,CAAc;IAC5B,CAAC;IAEJ,sBAAsB,CAAC,QAAwB;QAC7C,MAAM,GAAG,GAAG,GAAG,QAAQ,CAAC,EAAE,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;QACjD,IAAI,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACnC,OAAO,CAAC,IAAI,CAAC,qCAAqC,GAAG,kCAAkC,CAAC,CAAC;YACzF,OAAO;QACT,CAAC;QACD,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC3C,CAAC;IAED,uBAAuB,CAAC,QAAyB;QAC/C,IAAI,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;YAC5C,OAAO,CAAC,IAAI,CAAC,sCAAsC,QAAQ,CAAC,EAAE,kCAAkC,CAAC,CAAC;YAClG,OAAO;QACT,CAAC;QACD,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;IACpD,CAAC;IAED,oBAAoB,CAAC,KAAmB;QACtC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED,kBAAkB,KAAyB,OAAO,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACxF,mBAAmB,KAAwB,OAAO,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACzF,gBAAgB,KAA2B,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CACvE"}
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import type { Entity } from './types.js';
3
+ interface EntityContextValue {
4
+ entity: Entity;
5
+ }
6
+ export declare const EntityContext: React.Context<EntityContextValue | null>;
7
+ export declare function EntityProvider({ entity, children, }: {
8
+ entity: Entity;
9
+ children: React.ReactNode;
10
+ }): import("react/jsx-runtime").JSX.Element;
11
+ interface PluginConfigContextValue {
12
+ get<T = unknown>(key: string): T | undefined;
13
+ }
14
+ export declare const PluginConfigContext: React.Context<PluginConfigContextValue>;
15
+ export declare function PluginConfigProvider({ config, children, }: {
16
+ config: Record<string, unknown>;
17
+ children: React.ReactNode;
18
+ }): import("react/jsx-runtime").JSX.Element;
19
+ /** @internal Used by useEntity hook */
20
+ export declare function useEntityContext(): EntityContextValue | null;
21
+ /** @internal Used by useConfig hook */
22
+ export declare function usePluginConfigContext(): PluginConfigContextValue;
23
+ export {};
24
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoC,MAAM,OAAO,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAIzC,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,aAAa,0CAAiD,CAAC;AAE5E,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAI,MAAM,CAAC;IACjB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,2CAMA;AAID,UAAU,wBAAwB;IAChC,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CAC9C;AAMD,eAAO,MAAM,mBAAmB,yCAAyD,CAAC;AAE1F,wBAAgB,oBAAoB,CAAC,EACnC,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,2CASA;AAED,uCAAuC;AACvC,wBAAgB,gBAAgB,IAAI,kBAAkB,GAAG,IAAI,CAE5D;AAED,uCAAuC;AACvC,wBAAgB,sBAAsB,IAAI,wBAAwB,CAEjE"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
3
+ export const EntityContext = createContext(null);
4
+ export function EntityProvider({ entity, children, }) {
5
+ return (_jsx(EntityContext.Provider, { value: { entity }, children: children }));
6
+ }
7
+ const defaultConfig = {
8
+ get: () => undefined,
9
+ };
10
+ export const PluginConfigContext = createContext(defaultConfig);
11
+ export function PluginConfigProvider({ config, children, }) {
12
+ const accessor = {
13
+ get: (key) => config[key],
14
+ };
15
+ return (_jsx(PluginConfigContext.Provider, { value: accessor, children: children }));
16
+ }
17
+ /** @internal Used by useEntity hook */
18
+ export function useEntityContext() {
19
+ return useContext(EntityContext);
20
+ }
21
+ /** @internal Used by useConfig hook */
22
+ export function usePluginConfigContext() {
23
+ return useContext(PluginConfigContext);
24
+ }
25
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AASzD,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAA4B,IAAI,CAAC,CAAC;AAE5E,MAAM,UAAU,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,GAIT;IACC,OAAO,CACL,KAAC,aAAa,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,MAAM,EAAE,YACtC,QAAQ,GACc,CAC1B,CAAC;AACJ,CAAC;AAQD,MAAM,aAAa,GAA6B;IAC9C,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS;CACrB,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAA2B,aAAa,CAAC,CAAC;AAE1F,MAAM,UAAU,oBAAoB,CAAC,EACnC,MAAM,EACN,QAAQ,GAIT;IACC,MAAM,QAAQ,GAA6B;QACzC,GAAG,EAAE,CAAc,GAAW,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAkB;KAChE,CAAC;IACF,OAAO,CACL,KAAC,mBAAmB,CAAC,QAAQ,IAAC,KAAK,EAAE,QAAQ,YAC1C,QAAQ,GACoB,CAChC,CAAC;AACJ,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,gBAAgB;IAC9B,OAAO,UAAU,CAAC,aAAa,CAAC,CAAC;AACnC,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,sBAAsB;IACpC,OAAO,UAAU,CAAC,mBAAmB,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,6 @@
1
+ export type { Entity, EntityDraft, EntityTab, EntityTabAppliesTo, EntityCard, Route, JsonSchema, JsonSchemaType, ActionResult, ActionLogger, ActionScmAccessor, ActionDbAccessor, ActionConfigAccessor, ActionContext, ActionProvider, CatalogProviderContext, CatalogProvider, ForgePluginSDK, PluginConfigFieldSchema, PluginCapabilities, PluginManifest, } from './types.js';
2
+ export { PluginRegistry, globalRegistry } from './registry.js';
3
+ export type { BackendRoute, ForgeBackendPluginSDK } from './backend.js';
4
+ export { BackendPluginRegistry } from './backend.js';
5
+ export declare const SDK_VERSION = "1.0.0";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,MAAM,EACN,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,UAAU,EACV,KAAK,EACL,UAAU,EACV,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,sBAAsB,EACtB,eAAe,EACf,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,cAAc,GACf,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/D,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAGrD,eAAO,MAAM,WAAW,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // ─── Registry ─────────────────────────────────────────────────────────────────
2
+ export { PluginRegistry, globalRegistry } from './registry.js';
3
+ export { BackendPluginRegistry } from './backend.js';
4
+ // ─── SDK version (used by plugin loader for engineVersion compatibility check) ─
5
+ export const SDK_VERSION = '1.0.0';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAyBA,iFAAiF;AACjF,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAI/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,kFAAkF;AAClF,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { type UseQueryOptions, type UseQueryResult } from '@tanstack/react-query';
2
+ import type { Entity } from './types.js';
3
+ export { EntityProvider, EntityContext, PluginConfigProvider, PluginConfigContext, } from './context.js';
4
+ /**
5
+ * Returns the current entity from the entity detail page context.
6
+ * Must be used inside a component rendered as an EntityTab or EntityCard.
7
+ *
8
+ * @throws if called outside of an EntityProvider.
9
+ */
10
+ export declare function useEntity(): {
11
+ entity: Entity;
12
+ };
13
+ /**
14
+ * Returns the plugin-scoped config value for the given key.
15
+ * Config is sourced from `forgeportal.yaml` -> `plugins.<pluginId>.config`.
16
+ *
17
+ * @example
18
+ * const apiUrl = useConfig<string>('apiEndpoint');
19
+ */
20
+ export declare function useConfig<T = unknown>(key: string): T | undefined;
21
+ /**
22
+ * Typed API fetcher backed by TanStack Query.
23
+ * Automatically includes credentials for session-based auth.
24
+ *
25
+ * @param path - Absolute API path, e.g. '/api/v1/entities'
26
+ * @param options - Optional TanStack Query options to override defaults
27
+ *
28
+ * @example
29
+ * const { data, isPending } = useApi<MyResponse[]>('/api/v1/my-plugin/data');
30
+ */
31
+ export declare function useApi<TData = unknown>(path: string, options?: Omit<UseQueryOptions<TData>, 'queryKey' | 'queryFn'>): UseQueryResult<TData, Error>;
32
+ //# sourceMappingURL=react.d.ts.map