@fayz-ai/core 0.1.6
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/dist/app/render.d.ts +16 -0
- package/dist/app/render.d.ts.map +1 -0
- package/dist/blocks/index.d.ts +25 -0
- package/dist/blocks/index.d.ts.map +1 -0
- package/dist/chunk-2NTHNWEH.cjs +32 -0
- package/dist/chunk-2NTHNWEH.cjs.map +1 -0
- package/dist/chunk-5EAZOMVP.js +1609 -0
- package/dist/chunk-5EAZOMVP.js.map +1 -0
- package/dist/chunk-5TKIZVVJ.cjs +16 -0
- package/dist/chunk-5TKIZVVJ.cjs.map +1 -0
- package/dist/chunk-7RTUHSKY.js +3 -0
- package/dist/chunk-7RTUHSKY.js.map +1 -0
- package/dist/chunk-AXTWKF5X.cjs +38 -0
- package/dist/chunk-AXTWKF5X.cjs.map +1 -0
- package/dist/chunk-EGWWQ7AT.cjs +355 -0
- package/dist/chunk-EGWWQ7AT.cjs.map +1 -0
- package/dist/chunk-KAYQFSR6.js +298 -0
- package/dist/chunk-KAYQFSR6.js.map +1 -0
- package/dist/chunk-LSVBDYFQ.cjs +1639 -0
- package/dist/chunk-LSVBDYFQ.cjs.map +1 -0
- package/dist/chunk-SNQW6TRF.cjs +658 -0
- package/dist/chunk-SNQW6TRF.cjs.map +1 -0
- package/dist/chunk-WKZVSKEU.js +26 -0
- package/dist/chunk-WKZVSKEU.js.map +1 -0
- package/dist/chunk-Y2C2FBQP.js +35 -0
- package/dist/chunk-Y2C2FBQP.js.map +1 -0
- package/dist/chunk-YSBXGHAM.js +643 -0
- package/dist/chunk-YSBXGHAM.js.map +1 -0
- package/dist/data/archetype.d.ts +13 -0
- package/dist/data/archetype.d.ts.map +1 -0
- package/dist/data/cached.d.ts +19 -0
- package/dist/data/cached.d.ts.map +1 -0
- package/dist/data/fayz-api.d.ts +19 -0
- package/dist/data/fayz-api.d.ts.map +1 -0
- package/dist/data/index.cjs +32 -0
- package/dist/data/index.cjs.map +1 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +3 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/mock.d.ts +7 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/resolve.d.ts +22 -0
- package/dist/data/resolve.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +18 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +23 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/entity/index.cjs +28 -0
- package/dist/entity/index.cjs.map +1 -0
- package/dist/entity/index.d.ts +3 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/index.js +3 -0
- package/dist/entity/index.js.map +1 -0
- package/dist/entity/registry.d.ts +20 -0
- package/dist/entity/registry.d.ts.map +1 -0
- package/dist/events/index.d.ts +16 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/i18n/index.cjs +44 -0
- package/dist/i18n/index.cjs.map +1 -0
- package/dist/i18n/index.d.ts +21 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/shell-translations.d.ts +2 -0
- package/dist/i18n/shell-translations.d.ts.map +1 -0
- package/dist/index.cjs +911 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +596 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cache.d.ts +21 -0
- package/dist/lib/cache.d.ts.map +1 -0
- package/dist/lib/csv.d.ts +14 -0
- package/dist/lib/csv.d.ts.map +1 -0
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/manifest/index.d.ts +80 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/plugin/createSafeDataProvider.d.ts +14 -0
- package/dist/plugin/createSafeDataProvider.d.ts.map +1 -0
- package/dist/plugin/index.cjs +32 -0
- package/dist/plugin/index.cjs.map +1 -0
- package/dist/plugin/index.d.ts +2 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +3 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/runtime.d.ts +31 -0
- package/dist/plugin/runtime.d.ts.map +1 -0
- package/dist/registry/index.d.ts +88 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/router/index.cjs +16 -0
- package/dist/router/index.cjs.map +1 -0
- package/dist/router/index.d.ts +8 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +3 -0
- package/dist/router/index.js.map +1 -0
- package/dist/runtime/index.cjs +16 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/oauth.d.ts +96 -0
- package/dist/runtime/oauth.d.ts.map +1 -0
- package/dist/tenant/index.d.ts +3 -0
- package/dist/tenant/index.d.ts.map +1 -0
- package/dist/types/auth.d.ts +27 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/billing.d.ts +17 -0
- package/dist/types/billing.d.ts.map +1 -0
- package/dist/types/crud.d.ts +107 -0
- package/dist/types/crud.d.ts.map +1 -0
- package/dist/types/entities.d.ts +134 -0
- package/dist/types/entities.d.ts.map +1 -0
- package/dist/types/i18n.d.ts +8 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/index.cjs +4 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/org.d.ts +96 -0
- package/dist/types/org.d.ts.map +1 -0
- package/dist/types/permissions.d.ts +25 -0
- package/dist/types/permissions.d.ts.map +1 -0
- package/dist/types/plugins.d.ts +314 -0
- package/dist/types/plugins.d.ts.map +1 -0
- package/dist/types/theme.d.ts +16 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/package.json +98 -0
- package/src/app/render.tsx +69 -0
- package/src/blocks/index.tsx +68 -0
- package/src/data/archetype.ts +210 -0
- package/src/data/cached.ts +55 -0
- package/src/data/fayz-api.ts +200 -0
- package/src/data/index.ts +8 -0
- package/src/data/mock.ts +84 -0
- package/src/data/resolve.ts +108 -0
- package/src/data/supabase.ts +169 -0
- package/src/data/types.ts +20 -0
- package/src/entity/index.ts +2 -0
- package/src/entity/registry.ts +42 -0
- package/src/events/index.tsx +82 -0
- package/src/i18n/index.ts +250 -0
- package/src/i18n/shell-translations.ts +1391 -0
- package/src/index.ts +132 -0
- package/src/lib/cache.ts +118 -0
- package/src/lib/csv.ts +54 -0
- package/src/lib/format.ts +58 -0
- package/src/manifest/app-manifest.schema.json +103 -0
- package/src/manifest/index.ts +394 -0
- package/src/plugin/createSafeDataProvider.ts +30 -0
- package/src/plugin/index.ts +1 -0
- package/src/plugin/runtime.ts +313 -0
- package/src/registry/index.ts +163 -0
- package/src/router/index.ts +29 -0
- package/src/runtime/index.ts +15 -0
- package/src/runtime/oauth.ts +219 -0
- package/src/tenant/index.ts +18 -0
- package/src/types/auth.ts +26 -0
- package/src/types/billing.ts +17 -0
- package/src/types/crud.ts +108 -0
- package/src/types/entities.ts +142 -0
- package/src/types/i18n.ts +7 -0
- package/src/types/index.ts +10 -0
- package/src/types/org.ts +94 -0
- package/src/types/permissions.ts +32 -0
- package/src/types/plugins.ts +363 -0
- package/src/types/theme.ts +17 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import type {
|
|
3
|
+
PluginManifest,
|
|
4
|
+
PluginRuntime,
|
|
5
|
+
PluginRuntimeContext,
|
|
6
|
+
PluginRuntimeIssue,
|
|
7
|
+
PluginRuntimeRoute,
|
|
8
|
+
PluginSettingsTab,
|
|
9
|
+
PluginWidgetDefinition,
|
|
10
|
+
PluginWidgetZone,
|
|
11
|
+
ResolvedPluginManifest,
|
|
12
|
+
ResolvedPluginWidget,
|
|
13
|
+
TenantPluginBinding,
|
|
14
|
+
PluginRegistryDef,
|
|
15
|
+
PluginAITool,
|
|
16
|
+
PluginNavigationEntry,
|
|
17
|
+
PluginCapability,
|
|
18
|
+
PluginWidgetVisibility,
|
|
19
|
+
ResolvedDashboardWidget,
|
|
20
|
+
DashboardSurface,
|
|
21
|
+
} from '../types/plugins'
|
|
22
|
+
import type { FeatureDeclaration } from '../types/permissions'
|
|
23
|
+
import { getComponent } from '../registry'
|
|
24
|
+
|
|
25
|
+
/** The plugin contract version this runtime understands. A plugin declaring a
|
|
26
|
+
* higher apiVersion is rejected rather than mis-rendered. */
|
|
27
|
+
export const PLUGIN_API_VERSION = 1
|
|
28
|
+
|
|
29
|
+
/** Resolve a plugin route/widget/settings/onboarding target to a component,
|
|
30
|
+
* whether it provided the component directly or a registered component id. */
|
|
31
|
+
export function resolvePluginComponent(
|
|
32
|
+
def: { component?: React.ComponentType<unknown>; componentId?: string },
|
|
33
|
+
): React.ComponentType<unknown> | undefined {
|
|
34
|
+
if (def.component) return def.component
|
|
35
|
+
if (def.componentId) return getComponent(def.componentId) as React.ComponentType<unknown> | undefined
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ResolvePluginRuntimeOptions {
|
|
40
|
+
plugins?: PluginManifest[]
|
|
41
|
+
tenantPlugins?: TenantPluginBinding[]
|
|
42
|
+
hasTenantBindings?: boolean
|
|
43
|
+
context: PluginRuntimeContext
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const EMPTY_CONTEXT: PluginRuntimeContext = {
|
|
47
|
+
tenant: null,
|
|
48
|
+
user: null,
|
|
49
|
+
currentPath: '/',
|
|
50
|
+
matchedPath: '/',
|
|
51
|
+
layout: 'sidebar',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function slugify(input: string): string {
|
|
55
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function matchesRoutePattern(pattern: string, currentPath: string): boolean {
|
|
59
|
+
if (pattern === currentPath) return true
|
|
60
|
+
if (pattern.endsWith('/*')) {
|
|
61
|
+
const base = pattern.slice(0, -2)
|
|
62
|
+
return currentPath === base || currentPath.startsWith(`${base}/`)
|
|
63
|
+
}
|
|
64
|
+
if (pattern.endsWith('*')) return currentPath.startsWith(pattern.slice(0, -1))
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isWidgetVisible(widget: ResolvedPluginWidget, context: PluginRuntimeContext): boolean {
|
|
69
|
+
if (widget.permission && context.hasPermission && !context.hasPermission(widget.permission)) return false
|
|
70
|
+
const visibility: PluginWidgetVisibility | undefined = widget.visibility
|
|
71
|
+
if (!visibility) return true
|
|
72
|
+
|
|
73
|
+
if (visibility.layouts && !visibility.layouts.includes(context.layout)) return false
|
|
74
|
+
if (visibility.roles && context.user?.role && !visibility.roles.includes(context.user.role)) return false
|
|
75
|
+
if (visibility.roles && !context.user?.role) return false
|
|
76
|
+
if (visibility.plans && context.tenant?.plan && !visibility.plans.includes(context.tenant.plan)) return false
|
|
77
|
+
if (visibility.plans && !context.tenant?.plan) return false
|
|
78
|
+
if (visibility.routes && !visibility.routes.some((p) => matchesRoutePattern(p, context.currentPath))) return false
|
|
79
|
+
if (visibility.excludeRoutes?.some((p) => matchesRoutePattern(p, context.currentPath))) return false
|
|
80
|
+
if (visibility.permissions?.length) {
|
|
81
|
+
if (!context.hasPermission) return false
|
|
82
|
+
if (!visibility.permissions.every((req) => context.hasPermission?.(req))) return false
|
|
83
|
+
}
|
|
84
|
+
if (visibility.when && !visibility.when(context)) return false
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeSettingsTabs(plugin: PluginManifest): PluginSettingsTab[] {
|
|
89
|
+
return (plugin.settings ?? []).map((entry, index) => ({
|
|
90
|
+
...entry,
|
|
91
|
+
pluginId: entry.pluginId ?? plugin.id,
|
|
92
|
+
order: entry.order ?? index,
|
|
93
|
+
id: entry.id ?? `${plugin.id}-${slugify(entry.label)}`,
|
|
94
|
+
}))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createEmptyRuntime(context: PluginRuntimeContext = EMPTY_CONTEXT): PluginRuntime {
|
|
98
|
+
return {
|
|
99
|
+
context,
|
|
100
|
+
plugins: [],
|
|
101
|
+
activePlugins: [],
|
|
102
|
+
routes: [],
|
|
103
|
+
navigation: [],
|
|
104
|
+
settingsTabs: [],
|
|
105
|
+
widgets: [],
|
|
106
|
+
dashboardWidgets: [],
|
|
107
|
+
capabilities: [],
|
|
108
|
+
aiTools: [],
|
|
109
|
+
registries: new Map(),
|
|
110
|
+
pluginFeatures: [],
|
|
111
|
+
issues: [],
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function definePlugin<T extends PluginManifest>(plugin: T): T {
|
|
116
|
+
return plugin
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function resolvePluginRuntime({
|
|
120
|
+
plugins = [],
|
|
121
|
+
tenantPlugins = [],
|
|
122
|
+
hasTenantBindings,
|
|
123
|
+
context,
|
|
124
|
+
}: ResolvePluginRuntimeOptions): PluginRuntime {
|
|
125
|
+
if (plugins.length === 0) return createEmptyRuntime(context)
|
|
126
|
+
|
|
127
|
+
const issues: PluginRuntimeIssue[] = []
|
|
128
|
+
const normalizedPlugins = new Map<string, PluginManifest>()
|
|
129
|
+
|
|
130
|
+
for (const plugin of plugins) {
|
|
131
|
+
if (normalizedPlugins.has(plugin.id)) {
|
|
132
|
+
issues.push({ type: 'duplicate_plugin', pluginId: plugin.id, message: `Plugin "${plugin.id}" registered more than once.` })
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (plugin.apiVersion != null && plugin.apiVersion > PLUGIN_API_VERSION) {
|
|
136
|
+
issues.push({
|
|
137
|
+
type: 'incompatible_api_version',
|
|
138
|
+
pluginId: plugin.id,
|
|
139
|
+
message: `Plugin "${plugin.id}" requires plugin API v${plugin.apiVersion}, but this runtime supports v${PLUGIN_API_VERSION}. Upgrade @fayz-ai/app-runtime.`,
|
|
140
|
+
})
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
normalizedPlugins.set(plugin.id, plugin)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const bindings = new Map<string, TenantPluginBinding>()
|
|
147
|
+
for (const b of tenantPlugins) bindings.set(b.pluginId, b)
|
|
148
|
+
const bindingsAreManaged = hasTenantBindings ?? tenantPlugins.length > 0
|
|
149
|
+
|
|
150
|
+
const requested = new Set<string>()
|
|
151
|
+
const activationReason = new Map<string, ResolvedPluginManifest['activationReason']>()
|
|
152
|
+
|
|
153
|
+
for (const plugin of normalizedPlugins.values()) {
|
|
154
|
+
const binding = bindings.get(plugin.id)
|
|
155
|
+
if (binding?.status === 'active') {
|
|
156
|
+
requested.add(plugin.id)
|
|
157
|
+
activationReason.set(plugin.id, 'tenant')
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if (plugin.defaultEnabled === true || (!bindingsAreManaged && plugin.defaultEnabled !== false)) {
|
|
161
|
+
requested.add(plugin.id)
|
|
162
|
+
activationReason.set(plugin.id, 'default')
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const orderedIds: string[] = []
|
|
167
|
+
const visiting = new Set<string>()
|
|
168
|
+
const visited = new Set<string>()
|
|
169
|
+
|
|
170
|
+
function visit(pluginId: string, ancestry: string[] = []): void {
|
|
171
|
+
if (visited.has(pluginId)) return
|
|
172
|
+
const plugin = normalizedPlugins.get(pluginId)
|
|
173
|
+
if (!plugin) return
|
|
174
|
+
if (visiting.has(pluginId)) {
|
|
175
|
+
issues.push({ type: 'circular_dependency', pluginId, message: `Circular dependency: ${[...ancestry, pluginId].join(' -> ')}.` })
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
visiting.add(pluginId)
|
|
179
|
+
for (const depId of plugin.dependencies ?? []) {
|
|
180
|
+
if (!normalizedPlugins.has(depId)) {
|
|
181
|
+
issues.push({ type: 'missing_dependency', pluginId, dependencyId: depId, message: `Plugin "${plugin.id}" depends on missing plugin "${depId}".` })
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
if (requested.has(pluginId) && !requested.has(depId)) {
|
|
185
|
+
requested.add(depId)
|
|
186
|
+
activationReason.set(depId, 'dependency')
|
|
187
|
+
}
|
|
188
|
+
visit(depId, [...ancestry, pluginId])
|
|
189
|
+
}
|
|
190
|
+
visiting.delete(pluginId)
|
|
191
|
+
visited.add(pluginId)
|
|
192
|
+
orderedIds.push(pluginId)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const id of normalizedPlugins.keys()) visit(id)
|
|
196
|
+
|
|
197
|
+
const resolvedById = new Map<string, ResolvedPluginManifest>()
|
|
198
|
+
|
|
199
|
+
for (const pluginId of orderedIds) {
|
|
200
|
+
const plugin = normalizedPlugins.get(pluginId)
|
|
201
|
+
if (!plugin) continue
|
|
202
|
+
const binding = bindings.get(plugin.id)
|
|
203
|
+
const missing = (plugin.dependencies ?? []).filter((id) => !normalizedPlugins.has(id))
|
|
204
|
+
const compatible = !plugin.verticalId || plugin.verticalId === context.tenant?.verticalId
|
|
205
|
+
if (!compatible) {
|
|
206
|
+
issues.push({ type: 'vertical_mismatch', pluginId: plugin.id, message: `Plugin "${plugin.id}" requires vertical "${plugin.verticalId}".` })
|
|
207
|
+
}
|
|
208
|
+
const reason = activationReason.get(plugin.id) ?? 'inactive'
|
|
209
|
+
const depsReady = (plugin.dependencies ?? []).every((id) => resolvedById.get(id)?.isActive)
|
|
210
|
+
const explicitlyDisabled = binding?.status === 'disabled' || binding?.status === 'removed'
|
|
211
|
+
const shouldActivate = reason === 'dependency' ? true : requested.has(plugin.id) && !explicitlyDisabled
|
|
212
|
+
const isActive = shouldActivate && compatible && missing.length === 0 && depsReady
|
|
213
|
+
|
|
214
|
+
resolvedById.set(plugin.id, {
|
|
215
|
+
...plugin,
|
|
216
|
+
status: isActive ? 'active' : (binding?.status ?? 'disabled'),
|
|
217
|
+
isActive,
|
|
218
|
+
activationReason: isActive ? reason : 'inactive',
|
|
219
|
+
config: binding?.config ?? {},
|
|
220
|
+
missingDependencies: missing,
|
|
221
|
+
settingsTabs: normalizeSettingsTabs(plugin),
|
|
222
|
+
widgets: plugin.widgets ?? [],
|
|
223
|
+
resolvedRegistries: plugin.registries ?? [],
|
|
224
|
+
resolvedAITools: plugin.aiTools ?? [],
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const resolvedPlugins = orderedIds
|
|
229
|
+
.map((id) => resolvedById.get(id))
|
|
230
|
+
.filter((p): p is ResolvedPluginManifest => Boolean(p))
|
|
231
|
+
|
|
232
|
+
const activePlugins = resolvedPlugins.filter((p) => p.isActive)
|
|
233
|
+
const routes: PluginRuntimeRoute[] = []
|
|
234
|
+
const navigation: PluginNavigationEntry[] = []
|
|
235
|
+
const settingsTabs: PluginSettingsTab[] = []
|
|
236
|
+
const widgets: ResolvedPluginWidget[] = []
|
|
237
|
+
const dashboardWidgets: ResolvedDashboardWidget[] = []
|
|
238
|
+
const capabilities: PluginCapability[] = []
|
|
239
|
+
const aiTools: PluginAITool[] = []
|
|
240
|
+
const registries = new Map<string, PluginRegistryDef[]>()
|
|
241
|
+
const pluginFeatures: FeatureDeclaration[] = []
|
|
242
|
+
|
|
243
|
+
for (const plugin of activePlugins) {
|
|
244
|
+
routes.push(...plugin.routes.map((r) => ({ ...r, plugin })))
|
|
245
|
+
navigation.push(...plugin.navigation.map((entry, i) => ({
|
|
246
|
+
...entry,
|
|
247
|
+
id: entry.id ?? `${plugin.id}:${entry.route}:${i}`,
|
|
248
|
+
icon: entry.icon ?? plugin.icon,
|
|
249
|
+
})))
|
|
250
|
+
settingsTabs.push(...plugin.settingsTabs)
|
|
251
|
+
capabilities.push(...(plugin.capabilities ?? []))
|
|
252
|
+
aiTools.push(...plugin.resolvedAITools)
|
|
253
|
+
if (plugin.resolvedRegistries.length > 0) registries.set(plugin.id, plugin.resolvedRegistries)
|
|
254
|
+
if (plugin.declaredFeatures) pluginFeatures.push(...plugin.declaredFeatures)
|
|
255
|
+
widgets.push(...plugin.widgets.map((w, i) => ({
|
|
256
|
+
...w,
|
|
257
|
+
order: w.order ?? i,
|
|
258
|
+
config: { ...plugin.config, ...(w.props ?? {}) },
|
|
259
|
+
plugin,
|
|
260
|
+
})))
|
|
261
|
+
dashboardWidgets.push(...(plugin.dashboardWidgets ?? []).map((w, i) => ({
|
|
262
|
+
...w,
|
|
263
|
+
domain: w.domain ?? plugin.id,
|
|
264
|
+
order: w.defaultOrder ?? i,
|
|
265
|
+
plugin,
|
|
266
|
+
})))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
navigation.sort((a, b) => {
|
|
270
|
+
const order = { main: 0, secondary: 1, settings: 2 }
|
|
271
|
+
return (order[a.section] ?? 99) - (order[b.section] ?? 99) || a.position - b.position
|
|
272
|
+
})
|
|
273
|
+
settingsTabs.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
274
|
+
widgets.sort((a, b) => a.zone !== b.zone ? a.zone.localeCompare(b.zone) : a.order - b.order)
|
|
275
|
+
dashboardWidgets.sort((a, b) => a.order - b.order)
|
|
276
|
+
|
|
277
|
+
return { context, plugins: resolvedPlugins, activePlugins, routes, navigation, settingsTabs, widgets, dashboardWidgets, capabilities, aiTools, issues, registries, pluginFeatures }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function getWidgetsForZone(
|
|
281
|
+
runtime: PluginRuntime,
|
|
282
|
+
zone: PluginWidgetZone,
|
|
283
|
+
contextOverrides?: Partial<PluginRuntimeContext>,
|
|
284
|
+
): ResolvedPluginWidget[] {
|
|
285
|
+
const ctx = { ...runtime.context, ...contextOverrides }
|
|
286
|
+
return runtime.widgets.filter((w) => w.zone === zone && isWidgetVisible(w, ctx))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function getDashboardWidgets(
|
|
290
|
+
runtime: PluginRuntime,
|
|
291
|
+
options: { surface: DashboardSurface; domain?: string; contextOverrides?: Partial<PluginRuntimeContext> },
|
|
292
|
+
): ResolvedDashboardWidget[] {
|
|
293
|
+
const ctx = { ...runtime.context, ...options.contextOverrides }
|
|
294
|
+
return runtime.dashboardWidgets.filter((w) => {
|
|
295
|
+
const surfaces = w.surfaces ?? ['home', 'plugin-home']
|
|
296
|
+
if (!surfaces.includes(options.surface)) return false
|
|
297
|
+
if (options.domain && w.domain !== options.domain) return false
|
|
298
|
+
if (w.verticalId && ctx.tenant?.verticalId && w.verticalId !== ctx.tenant.verticalId) return false
|
|
299
|
+
if (w.permission && ctx.hasPermission && !ctx.hasPermission(w.permission)) return false
|
|
300
|
+
return true
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const PluginRuntimeContext = React.createContext<PluginRuntime | null>(null)
|
|
305
|
+
export const PluginRuntimeProvider = PluginRuntimeContext.Provider
|
|
306
|
+
|
|
307
|
+
export function usePluginRuntime(): PluginRuntime {
|
|
308
|
+
return React.useContext(PluginRuntimeContext) ?? createEmptyRuntime()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function usePluginRuntimeOptional(): PluginRuntime | null {
|
|
312
|
+
return React.useContext(PluginRuntimeContext)
|
|
313
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { ComponentType } from 'react'
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Uniform registry — the indirection layer that lets a pure-data AppManifest
|
|
5
|
+
// reference code by string id. Generalizes the entity registry pattern to
|
|
6
|
+
// components, blocks, pages, metrics, scaffolds and plugins.
|
|
7
|
+
//
|
|
8
|
+
// Naming convention: SDK/plugin artifacts use a namespaced id
|
|
9
|
+
// (`agenda.week-view`, `crud.detail-header`); app-local custom code uses the
|
|
10
|
+
// reserved `custom:` prefix (`custom:tannat.HarvestBoard`). The platform never
|
|
11
|
+
// generates `custom:` ids — they only come from an app's registry.tsx.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** A JSON Schema object (loosely typed; validated by tooling, not at runtime). */
|
|
15
|
+
export type JsonSchema = Record<string, unknown>
|
|
16
|
+
|
|
17
|
+
export type RegistrySource = 'sdk' | 'plugin' | 'app'
|
|
18
|
+
|
|
19
|
+
export interface RegistryMeta {
|
|
20
|
+
/** Human label for the platform editor. */
|
|
21
|
+
label?: string
|
|
22
|
+
description?: string
|
|
23
|
+
/** Where this artifact came from (for introspection / conflict reporting). */
|
|
24
|
+
source?: RegistrySource
|
|
25
|
+
/** Owning plugin id, when source is 'plugin'. */
|
|
26
|
+
pluginId?: string
|
|
27
|
+
/** JSON Schema for this artifact's props/config — drives the visual editor
|
|
28
|
+
* and constrains AI generation. Optional but strongly encouraged. */
|
|
29
|
+
propsSchema?: JsonSchema
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RegistryEntry<T> {
|
|
33
|
+
id: string
|
|
34
|
+
value: T
|
|
35
|
+
meta: RegistryMeta
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A typed, introspectable id→value map. Last registration wins (so an app's
|
|
39
|
+
* registry.tsx can override an SDK artifact by re-registering its id). */
|
|
40
|
+
export class Registry<T> {
|
|
41
|
+
private readonly entries = new Map<string, RegistryEntry<T>>()
|
|
42
|
+
|
|
43
|
+
constructor(readonly kind: string) {}
|
|
44
|
+
|
|
45
|
+
register(id: string, value: T, meta: RegistryMeta = {}): void {
|
|
46
|
+
this.entries.set(id, { id, value, meta })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get(id: string): T | undefined {
|
|
50
|
+
return this.entries.get(id)?.value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getEntry(id: string): RegistryEntry<T> | undefined {
|
|
54
|
+
return this.entries.get(id)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
has(id: string): boolean {
|
|
58
|
+
return this.entries.has(id)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
list(): RegistryEntry<T>[] {
|
|
62
|
+
return Array.from(this.entries.values())
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ids(): string[] {
|
|
66
|
+
return Array.from(this.entries.keys())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clear(): void {
|
|
70
|
+
this.entries.clear()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True for app-local custom artifacts (`custom:*`). */
|
|
75
|
+
export function isCustomId(id: string): boolean {
|
|
76
|
+
return id.startsWith('custom:')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Metric definitions (consumed by dashboard / reports / KPI panels).
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export type MetricFormat = 'number' | 'currency' | 'percent' | 'duration' | 'rating'
|
|
84
|
+
|
|
85
|
+
export interface MetricContext {
|
|
86
|
+
tenantId?: string
|
|
87
|
+
/** ISO date range the metric should compute over, when applicable. */
|
|
88
|
+
range?: { from: string; to: string }
|
|
89
|
+
[key: string]: unknown
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MetricValue {
|
|
93
|
+
value: number
|
|
94
|
+
previousValue?: number
|
|
95
|
+
trend?: 'up' | 'down' | 'flat'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface MetricDefinition {
|
|
99
|
+
id: string
|
|
100
|
+
label?: string
|
|
101
|
+
category?: string
|
|
102
|
+
format?: MetricFormat
|
|
103
|
+
compute: (ctx: MetricContext) => Promise<MetricValue> | MetricValue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Concrete registries. Components/blocks/pages/scaffolds hold React component
|
|
108
|
+
// types; metrics hold definitions; plugins hold opaque manifests (typed at the
|
|
109
|
+
// plugin layer to avoid a circular import).
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
type AnyComponent = ComponentType<any>
|
|
114
|
+
|
|
115
|
+
export const componentRegistry = new Registry<AnyComponent>('component')
|
|
116
|
+
export const blockRegistry = new Registry<AnyComponent>('block')
|
|
117
|
+
export const pageRegistry = new Registry<AnyComponent>('page')
|
|
118
|
+
export const scaffoldRegistry = new Registry<AnyComponent>('scaffold')
|
|
119
|
+
export const metricRegistry = new Registry<MetricDefinition>('metric')
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
121
|
+
export const pluginRegistry = new Registry<any>('plugin')
|
|
122
|
+
|
|
123
|
+
// Thin functional facade (the public API surface) -----------------------------
|
|
124
|
+
|
|
125
|
+
export const registerComponent = (id: string, c: AnyComponent, meta?: RegistryMeta) =>
|
|
126
|
+
componentRegistry.register(id, c, meta)
|
|
127
|
+
export const getComponent = (id: string) => componentRegistry.get(id)
|
|
128
|
+
|
|
129
|
+
export const registerBlock = (id: string, c: AnyComponent, meta?: RegistryMeta) =>
|
|
130
|
+
blockRegistry.register(id, c, meta)
|
|
131
|
+
export const getBlock = (id: string) => blockRegistry.get(id)
|
|
132
|
+
export const listBlocks = () => blockRegistry.list()
|
|
133
|
+
|
|
134
|
+
export const registerPage = (id: string, c: AnyComponent, meta?: RegistryMeta) =>
|
|
135
|
+
pageRegistry.register(id, c, meta)
|
|
136
|
+
export const getPage = (id: string) => pageRegistry.get(id)
|
|
137
|
+
|
|
138
|
+
export const registerScaffold = (id: string, c: AnyComponent, meta?: RegistryMeta) =>
|
|
139
|
+
scaffoldRegistry.register(id, c, meta)
|
|
140
|
+
export const getScaffold = (id: string) => scaffoldRegistry.get(id)
|
|
141
|
+
|
|
142
|
+
export const registerMetric = (def: MetricDefinition, meta?: RegistryMeta) =>
|
|
143
|
+
metricRegistry.register(def.id, def, meta)
|
|
144
|
+
export const getMetric = (id: string) => metricRegistry.get(id)
|
|
145
|
+
export const listMetrics = () => metricRegistry.list()
|
|
146
|
+
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
export const registerPlugin = (id: string, manifest: any, meta?: RegistryMeta) =>
|
|
149
|
+
pluginRegistry.register(id, manifest, meta)
|
|
150
|
+
export const getPlugin = (id: string) => pluginRegistry.get(id)
|
|
151
|
+
export const listPlugins = () => pluginRegistry.list()
|
|
152
|
+
|
|
153
|
+
// Plugin factories — a generated app registers each installed @fayz-ai/plugin-*
|
|
154
|
+
// factory by id (src/plugins.generated.ts), so a scaffold can resolve a
|
|
155
|
+
// manifest's PluginRef (id + JSON config) to a live PluginManifest:
|
|
156
|
+
// getPluginFactory(ref.id)?.(ref.config)
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
|
+
export type PluginFactory = (config?: Record<string, unknown>) => any
|
|
159
|
+
export const pluginFactoryRegistry = new Registry<PluginFactory>('plugin-factory')
|
|
160
|
+
export const registerPluginFactory = (id: string, factory: PluginFactory, meta?: RegistryMeta) =>
|
|
161
|
+
pluginFactoryRegistry.register(id, factory, meta)
|
|
162
|
+
export const getPluginFactory = (id: string) => pluginFactoryRegistry.get(id)
|
|
163
|
+
export const listPluginFactories = () => pluginFactoryRegistry.list()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface RouterAdapter {
|
|
2
|
+
navigate(to: string): void
|
|
3
|
+
getCurrentPath(): string
|
|
4
|
+
onPathChange(cb: (path: string) => void): () => void
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function hashRouterAdapter(): RouterAdapter {
|
|
8
|
+
return {
|
|
9
|
+
navigate(to) { window.location.hash = to },
|
|
10
|
+
getCurrentPath() { return window.location.hash.slice(1) || '/' },
|
|
11
|
+
onPathChange(cb) {
|
|
12
|
+
const handler = () => cb(window.location.hash.slice(1) || '/')
|
|
13
|
+
window.addEventListener('hashchange', handler)
|
|
14
|
+
return () => window.removeEventListener('hashchange', handler)
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function windowRouterAdapter(): RouterAdapter {
|
|
20
|
+
return {
|
|
21
|
+
navigate(to) { window.history.pushState(null, '', to) },
|
|
22
|
+
getCurrentPath() { return window.location.pathname },
|
|
23
|
+
onPathChange(cb) {
|
|
24
|
+
const handler = () => cb(window.location.pathname)
|
|
25
|
+
window.addEventListener('popstate', handler)
|
|
26
|
+
return () => window.removeEventListener('popstate', handler)
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createFayzRuntimeClient,
|
|
3
|
+
FayzRuntimeError,
|
|
4
|
+
} from '@fayz-ai/sdk/runtime'
|
|
5
|
+
export type {
|
|
6
|
+
FayzRuntimeClientOptions,
|
|
7
|
+
FayzRuntimeEnvironment,
|
|
8
|
+
GoogleCalendarEvent,
|
|
9
|
+
GoogleCalendarEventInput,
|
|
10
|
+
GoogleCalendarEventTime,
|
|
11
|
+
ListGoogleCalendarEventsInput,
|
|
12
|
+
PluginOAuthExchangeInput,
|
|
13
|
+
PluginOAuthExchangeResponse,
|
|
14
|
+
RuntimePluginOAuthGrant,
|
|
15
|
+
} from '@fayz-ai/sdk/runtime'
|