@zenk-agent/plugin-preview 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # @zenk-agent/plugin-preview
2
+
3
+ 单插件预览服务。运行后会启动一个 Vite dev server,并把当前目录当作插件根目录自动加载。
4
+
5
+ ## 用法
6
+
7
+ ```bash
8
+ zenk-plugin-preview dev
9
+ zenk-plugin-preview dev /path/to/plugin --port 4176 --host 127.0.0.1
10
+ ```
11
+
12
+ ## 约定
13
+
14
+ - 当前目录需要有 `package.json`
15
+ - `package.json` 中需要声明 `zenk.publicPlugin` 或 `zenk.publicPlugins`
16
+ - 对应的 `exports` 里需要能解析出 config 和 worker 入口
17
+
18
+ ## 代理配置
19
+
20
+ 预览模式下,插件里的 HTTP 请求应当直接写相对 URL,例如:
21
+
22
+ ```ts
23
+ import { usePluginSdk } from '@zenk-agent/plugin-sdk'
24
+
25
+ const sdk = usePluginSdk()
26
+
27
+ await sdk.http.request({
28
+ url: '/api/ontology/schemas/EQ/instances?limit=10&offset=0',
29
+ method: 'GET',
30
+ })
31
+ ```
32
+
33
+ 不要在插件 worker 里写死后端完整地址。预览服务会根据 `preview.config.json` 的 `proxy` 把这些前缀转发到真实后端。
34
+
35
+ 配置优先级:
36
+
37
+ 1. 当前插件目录 `preview.config.json`
38
+ 2. workspace 根目录 `preview.config.json`
39
+
40
+ 注意:`preview.config.json` 只在 `zenk-plugin-preview dev` 启动时读取一次。修改后需要重启预览服务,不能依赖 Vite 热更新刷新代理或 ontology 相关配置。
41
+
42
+ 推荐优先使用 `preview.config.json`:
43
+
44
+ ```json
45
+ {
46
+ "proxy": {
47
+ "/v1": {
48
+ "target": "http://127.0.0.1:8999",
49
+ "changeOrigin": true,
50
+ "secure": true
51
+ },
52
+ "/agent-api": {
53
+ "target": "http://127.0.0.1:8999",
54
+ "changeOrigin": true,
55
+ "secure": true
56
+ },
57
+ "/api": {
58
+ "target": "http://127.0.0.1:10000",
59
+ "changeOrigin": true,
60
+ "secure": false
61
+ },
62
+ "/tools-api": {
63
+ "target": "http://127.0.0.1:10000",
64
+ "changeOrigin": true,
65
+ "secure": false
66
+ }
67
+ },
68
+ "ontology": {
69
+ "mode": "remote",
70
+ "baseUrl": "http://127.0.0.1:9998",
71
+ "proxyPrefix": "/__plugin-preview-ontology",
72
+ "schemasPath": "/api/ontology/schemas"
73
+ }
74
+ }
75
+ ```
76
+
77
+ 其中:
78
+
79
+ - `proxy` 结构直接复用 Vite `server.proxy`
80
+ - 请求发哪个前缀,由插件代码自己决定
81
+ - 预览层不再维护“默认 API 前缀”
82
+
83
+ ## Ontology 调试数据源
84
+
85
+ `sdk.ontology.query()` / `sdk.ontology.get()` / `sdk.ontology.executeCypher()` 在预览环境支持两种模式:
86
+
87
+ 1. `mock`
88
+ 2. `remote`
89
+
90
+ 默认是 `mock`,会返回预览内置假数据。
91
+
92
+ 如果要联调真实后端,推荐在 `preview.config.json` 里配置:
93
+
94
+ ```json
95
+ {
96
+ "proxy": {
97
+ "/v1": {
98
+ "target": "http://192.168.10.61:8999",
99
+ "changeOrigin": true,
100
+ "secure": true
101
+ },
102
+ "/agent-api": {
103
+ "target": "http://192.168.10.61:8999",
104
+ "changeOrigin": true,
105
+ "secure": true
106
+ },
107
+ "/api": {
108
+ "target": "http://192.168.10.61:10000",
109
+ "changeOrigin": true,
110
+ "secure": false
111
+ },
112
+ "/tools-api": {
113
+ "target": "http://192.168.10.61:10000",
114
+ "changeOrigin": true,
115
+ "secure": false
116
+ }
117
+ },
118
+ "ontology": {
119
+ "mode": "remote",
120
+ "baseUrl": "http://192.168.10.61:9998"
121
+ }
122
+ }
123
+ ```
124
+ 此时 ontology capability 会请求:
125
+
126
+ ```text
127
+ GET /api/ontology/schemas/:schemaKey/instances?limit=20&offset=0
128
+ GET /api/ontology/schemas/:schemaKey/instances/:entityId
129
+ GET /api/ontology/schemas/:schemaKey/instances/by-name/:entityName
130
+ POST /api/ontology/schemas/:schemaKey/cypher (executeCypher,body: { cypher })
131
+ ```
132
+
133
+ 例如:
134
+
135
+ ```text
136
+ http://localhost:4175/api/ontology/schemas/SUP/instances?limit=20&offset=0
137
+ ```
138
+
139
+ 插件侧调用不需要改,仍然保持:
140
+
141
+ ```ts
142
+ sdk.ontology.query({ schemaKey: 'SUP', query: { limit: 20, offset: 0 } })
143
+ sdk.ontology.get({ schemaKey: 'SUP', entityId: '123' })
144
+ sdk.ontology.executeCypher({ cypher: 'MATCH (n:Equipment) RETURN n LIMIT 10' })
145
+ ```
package/app/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Zenk Plugin Preview</title>
7
+ </head>
8
+ <body class="bg-slate-950">
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,101 @@
1
+ import { useState } from 'react'
2
+ import { HostedPluginCard, RemotePluginCardHost, renderDefaultPluginModal } from '@zenk-agent/plugin-host'
3
+ import { decision, demoTheme, fetchFn, navigate, ontology } from './preview-host'
4
+ import { previewPluginDir, previewPlugins } from 'virtual:tool-plugin-preview'
5
+
6
+ function App() {
7
+ const [collapsed, setCollapsed] = useState(false)
8
+
9
+ return (
10
+ <main className="relative min-h-screen bg-[radial-gradient(circle_at_top,rgba(251,191,36,0.18),transparent_28%),linear-gradient(180deg,#111827_0%,#020617_100%)] text-slate-50">
11
+ {/* Left content area */}
12
+ <div className={`mx-auto flex w-full max-w-5xl flex-col gap-8 px-6 py-10 transition-[padding-right] duration-200 lg:px-10 ${collapsed ? 'pr-[72px]' : 'pr-[416px]'}`}>
13
+ <header className="max-w-3xl">
14
+ <p className="text-sm font-medium uppercase tracking-[0.24em] text-amber-300/80">Plugin Preview Service</p>
15
+ <h1 className="mt-3 text-4xl font-semibold tracking-tight text-white sm:text-5xl">Tool plugin preview</h1>
16
+ <p className="mt-4 text-base leading-7 text-slate-300">
17
+ 这个页面由 `@zenk-agent/plugin-preview` 启动,只预览当前插件目录。
18
+ </p>
19
+ <p className="mt-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-300">
20
+ <span className="font-semibold text-white">Plugin dir</span>: {previewPluginDir}
21
+ </p>
22
+ </header>
23
+
24
+ {previewPlugins.length === 0 ? (
25
+ <section className="rounded-[28px] border border-amber-300/20 bg-white/[0.04] px-6 py-6 shadow-[0_24px_60px_rgba(2,8,23,0.32)]">
26
+ <p className="text-lg font-semibold text-white">没有匹配到可预览的插件</p>
27
+ <p className="mt-2 text-sm leading-6 text-slate-300">
28
+ 当前目录需要包含 `package.json`,并在 `zenk.publicPlugin` 或 `zenk.publicPlugins` 中声明 config 和 worker 导出。
29
+ </p>
30
+ </section>
31
+ ) : null}
32
+ </div>
33
+
34
+ {/* Right toolbar */}
35
+ <aside
36
+ className={`absolute right-0 top-0 z-20 h-full border-l border-white/10 bg-white/[0.05] backdrop-blur-sm transition-[width] duration-200 ${collapsed ? 'w-14 overflow-hidden' : 'w-[400px]'}`}
37
+ >
38
+ {/* Toolbar header */}
39
+ <div
40
+ className={`flex h-[54px] items-center border-b border-white/10 bg-white/[0.02] ${collapsed ? 'justify-center px-2' : 'justify-between gap-2 px-6'}`}
41
+ >
42
+ <div className="flex items-center gap-2">
43
+ <svg className="h-4 w-4 text-amber-400" viewBox="0 0 16 16" fill="none" aria-hidden="true">
44
+ <path
45
+ d="M3 2.667h10v4.666H3V2.667Zm2 2H4.333V5.33H5V4.667Zm0 3.333h6.667v5.333H5V8Zm-2 0h1v5.333H3V8Zm10 0h-1v5.333h1V8Z"
46
+ fill="currentColor"
47
+ />
48
+ </svg>
49
+ {!collapsed ? <span className="text-[16px] font-medium text-white">插件预览</span> : null}
50
+ </div>
51
+ <button
52
+ type="button"
53
+ onClick={() => setCollapsed((prev) => !prev)}
54
+ className="inline-flex h-7 w-7 items-center justify-center rounded-md text-[#d7dbe4] transition hover:bg-white/10 hover:text-white"
55
+ aria-label={collapsed ? '展开工具栏' : '收起工具栏'}
56
+ title={collapsed ? '展开工具栏' : '收起工具栏'}
57
+ >
58
+ <svg className="h-4 w-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
59
+ <path
60
+ d={collapsed ? 'M6 3.5L10 8L6 12.5' : 'M10 3.5L6 8L10 12.5'}
61
+ stroke="currentColor"
62
+ strokeWidth="1.4"
63
+ strokeLinecap="round"
64
+ strokeLinejoin="round"
65
+ />
66
+ </svg>
67
+ </button>
68
+ </div>
69
+
70
+ {/* Toolbar content */}
71
+ {!collapsed ? (
72
+ <div className="h-[calc(100%-54px)] overflow-y-auto px-4 py-4 pb-6 scrollbar-thin">
73
+ <div className="space-y-4">
74
+ {previewPlugins.map((plugin) => (
75
+ <HostedPluginCard
76
+ key={plugin.id}
77
+ plugin={plugin}
78
+ renderPluginContent={() => (
79
+ <RemotePluginCardHost
80
+ plugin={plugin}
81
+ navigate={navigate}
82
+ fetchFn={fetchFn}
83
+ getTheme={() => demoTheme}
84
+ storage={window.localStorage}
85
+ decision={decision}
86
+ ontology={ontology}
87
+ renderModal={renderDefaultPluginModal}
88
+ styleIsolation="shadow"
89
+ />
90
+ )}
91
+ />
92
+ ))}
93
+ </div>
94
+ </div>
95
+ ) : null}
96
+ </aside>
97
+ </main>
98
+ )
99
+ }
100
+
101
+ export default App
@@ -0,0 +1,16 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App'
4
+ import './styles.css'
5
+
6
+ const container = document.getElementById('root')
7
+
8
+ if (!container) {
9
+ throw new Error('Root container #root was not found.')
10
+ }
11
+
12
+ createRoot(container).render(
13
+ <StrictMode>
14
+ <App />
15
+ </StrictMode>
16
+ )
@@ -0,0 +1,339 @@
1
+ import type {
2
+ OntologyPermission,
3
+ PluginDecisionPayload,
4
+ PluginHttpPayload,
5
+ PluginListItem,
6
+ PluginOntologyPayload,
7
+ PluginTheme,
8
+ } from '@zenk-agent/plugin-types'
9
+
10
+ export const demoTheme: PluginTheme = {
11
+ mode: 'dark',
12
+ tokens: {
13
+ accent: '#fbbf24',
14
+ surface: '#111827',
15
+ text: '#f8fafc',
16
+ },
17
+ }
18
+
19
+ export const navigate = (to: string, options?: { replace?: boolean }) => {
20
+ const mode = options?.replace ? 'replace' : 'push'
21
+ console.info(`[tool-plugins-preview:navigate] ${mode}`, to)
22
+ }
23
+
24
+ type OntologyPreviewMode = 'mock' | 'remote'
25
+ type DecisionPreviewMode = 'mock' | 'remote'
26
+
27
+ const trimTrailingSlash = (value: string) => value.replace(/\/+$/, '')
28
+ const normalizePath = (path: string) => (path.startsWith('/') ? path : `/${path}`)
29
+
30
+ const ontologyBaseUrl = trimTrailingSlash(String(import.meta.env.VITE_PLUGIN_ONTOLOGY_BASE_URL ?? ''))
31
+ const ontologyProxyPrefix = trimTrailingSlash(
32
+ String(import.meta.env.VITE_PLUGIN_ONTOLOGY_PROXY_PREFIX ?? '/__plugin-preview-ontology')
33
+ )
34
+ const ontologySchemasPath = String(import.meta.env.VITE_SERVER_ONTOLOGY_SCHEMAS_PATH ?? '/api/ontology/schemas')
35
+ const ontologyCypherQueryPath = String(import.meta.env.VITE_SERVER_ONTOLOGY_CYPHER_QUERY_PATH ?? '/api/ontology/cypher/query')
36
+ const decisionBaseUrl = trimTrailingSlash(String(import.meta.env.VITE_PLUGIN_DECISION_BASE_URL ?? ''))
37
+ const decisionProxyPrefix = trimTrailingSlash(
38
+ String(import.meta.env.VITE_PLUGIN_DECISION_PROXY_PREFIX ?? '/__plugin-preview-decision')
39
+ )
40
+ const decisionInvokePath = String(import.meta.env.VITE_SERVER_DECISION_INVOKE_PATH ?? '/api/decision/invoke')
41
+
42
+ const ontologyMode = ((): OntologyPreviewMode => {
43
+ const raw = String(import.meta.env.VITE_PLUGIN_ONTOLOGY_MODE ?? 'mock').trim().toLowerCase()
44
+ return raw === 'remote' ? 'remote' : 'mock'
45
+ })()
46
+
47
+ const decisionMode = ((): DecisionPreviewMode => {
48
+ const raw = String(import.meta.env.VITE_PLUGIN_DECISION_MODE ?? 'mock').trim().toLowerCase()
49
+ return raw === 'remote' ? 'remote' : 'mock'
50
+ })()
51
+
52
+ const readHttpResponse = async (response: Response) => {
53
+ if (response.status === 204) return null
54
+
55
+ const contentType = response.headers.get('content-type') ?? ''
56
+ if (contentType.includes('application/json')) {
57
+ return response.json()
58
+ }
59
+
60
+ return response.text()
61
+ }
62
+
63
+ const isApiEnvelope = <T>(value: unknown): value is { code: number; message: string; data: T } => {
64
+ if (!value || typeof value !== 'object') return false
65
+ const candidate = value as Partial<{ code: number; message: string; data: T }>
66
+ return typeof candidate.code === 'number' && typeof candidate.message === 'string' && 'data' in candidate
67
+ }
68
+
69
+ const unwrapApiResponse = <T>(payload: unknown): T => {
70
+ if (!isApiEnvelope<T>(payload)) {
71
+ throw new Error('Unexpected server response format')
72
+ }
73
+
74
+ if (payload.code !== 200) {
75
+ throw new Error(payload.message || `Request failed with code ${payload.code}`)
76
+ }
77
+
78
+ return payload.data
79
+ }
80
+
81
+ const fetchJson = async (input: string) => {
82
+ const response = await fetch(input, {
83
+ method: 'GET',
84
+ credentials: 'include',
85
+ })
86
+
87
+ if (!response.ok) {
88
+ const detail = await readHttpResponse(response)
89
+ throw new Error(
90
+ typeof detail === 'string' && detail.trim()
91
+ ? detail
92
+ : `Ontology request failed with status ${response.status}`
93
+ )
94
+ }
95
+ return unwrapApiResponse(await readHttpResponse(response))
96
+ }
97
+
98
+ const postJson = async (input: string, body: unknown) => {
99
+ const response = await fetch(input, {
100
+ method: 'POST',
101
+ credentials: 'include',
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ },
105
+ body: JSON.stringify(body),
106
+ })
107
+
108
+ if (!response.ok) {
109
+ const detail = await readHttpResponse(response)
110
+ throw new Error(
111
+ typeof detail === 'string' && detail.trim()
112
+ ? detail
113
+ : `Ontology request failed with status ${response.status}`
114
+ )
115
+ }
116
+ return unwrapApiResponse(await readHttpResponse(response))
117
+ }
118
+
119
+ const toFiniteNumber = (value: unknown, fallback: number, validator: (value: number) => boolean) => {
120
+ const parsed =
121
+ typeof value === 'number' && Number.isFinite(value)
122
+ ? value
123
+ : Number(value)
124
+
125
+ return Number.isFinite(parsed) && validator(parsed) ? parsed : fallback
126
+ }
127
+
128
+ const buildOntologyInstancesPath = (schemaKey: string) =>
129
+ `${normalizePath(ontologySchemasPath)}/${encodeURIComponent(schemaKey)}/instances`
130
+
131
+ const buildOntologyInstanceDetailPath = (
132
+ schemaKey: string,
133
+ { entityId, entityName }: { entityId?: string; entityName?: string }
134
+ ) => {
135
+ const normalizedEntityId = trimToUndefined(entityId)
136
+ if (normalizedEntityId) {
137
+ return `${buildOntologyInstancesPath(schemaKey)}/${encodeURIComponent(normalizedEntityId)}`
138
+ }
139
+
140
+ const normalizedEntityName = trimToUndefined(entityName)
141
+ if (normalizedEntityName) {
142
+ return `${buildOntologyInstancesPath(schemaKey)}/by-name/${encodeURIComponent(normalizedEntityName)}`
143
+ }
144
+
145
+ throw new Error('entityId or entityName is required')
146
+ }
147
+
148
+ const buildOntologyApiUrl = (path: string, query?: Record<string, unknown>) => {
149
+ const basePath = ontologyBaseUrl
150
+ ? `${normalizePath(ontologyProxyPrefix)}${normalizePath(path)}`
151
+ : normalizePath(path)
152
+ const url = new URL(basePath, window.location.origin)
153
+
154
+ if (query) {
155
+ for (const [key, value] of Object.entries(query)) {
156
+ if (value == null || value === '') continue
157
+
158
+ if (Array.isArray(value)) {
159
+ for (const item of value) {
160
+ if (item != null && item !== '') {
161
+ url.searchParams.append(key, String(item))
162
+ }
163
+ }
164
+ continue
165
+ }
166
+
167
+ url.searchParams.set(key, String(value))
168
+ }
169
+ }
170
+
171
+ return url.toString()
172
+ }
173
+
174
+ const buildDecisionApiUrl = (path: string) => {
175
+ const basePath = decisionBaseUrl
176
+ ? `${normalizePath(decisionProxyPrefix)}${normalizePath(path)}`
177
+ : normalizePath(path)
178
+ return new URL(basePath, window.location.origin).toString()
179
+ }
180
+
181
+ const trimToUndefined = (value?: string) => {
182
+ const nextValue = value?.trim()
183
+ return nextValue ? nextValue : undefined
184
+ }
185
+
186
+ const queryParamValue = (query: Record<string, unknown> | undefined, keys: string[]) => {
187
+ if (!query) return undefined
188
+
189
+ for (const key of keys) {
190
+ const value = query[key]
191
+ if (value != null && value !== '') return value
192
+ }
193
+
194
+ return undefined
195
+ }
196
+
197
+ export const previewOntologyConfig = {
198
+ mode: ontologyMode,
199
+ baseUrl: ontologyBaseUrl,
200
+ proxyPrefix: ontologyProxyPrefix,
201
+ schemasPath: ontologySchemasPath,
202
+ cypherQueryPath: ontologyCypherQueryPath,
203
+ }
204
+
205
+ export const previewDecisionConfig = {
206
+ mode: decisionMode,
207
+ baseUrl: decisionBaseUrl,
208
+ proxyPrefix: decisionProxyPrefix,
209
+ invokePath: decisionInvokePath,
210
+ }
211
+
212
+ export const fetchFn = async (request: PluginHttpPayload, context: { plugin: PluginListItem; token?: string | null }) => {
213
+ const headers = new Headers(request.headers ?? {})
214
+ if (context.token) headers.set('Authorization', `Bearer ${context.token}`)
215
+
216
+ console.info('[tool-plugins-preview:http]', context.plugin.id, request.url, {
217
+ method: request.method ?? 'GET',
218
+ token: context.token ?? null,
219
+ })
220
+
221
+ const response = await fetch(request.url, {
222
+ ...request,
223
+ headers,
224
+ credentials: request.credentials ?? 'include',
225
+ })
226
+
227
+ if (!response.ok) {
228
+ const detail = await readHttpResponse(response)
229
+ throw new Error(
230
+ typeof detail === 'string' && detail.trim()
231
+ ? detail
232
+ : `HTTP request failed with status ${response.status}`
233
+ )
234
+ }
235
+
236
+ return readHttpResponse(response)
237
+ }
238
+
239
+ export const decision = async (request: PluginDecisionPayload, context: { plugin: PluginListItem }) => {
240
+ console.info('[tool-plugins-preview:decision]', context.plugin.id, request)
241
+
242
+ if (decisionMode === 'remote') {
243
+ return postJson(buildDecisionApiUrl(decisionInvokePath), request)
244
+ }
245
+
246
+ if (request.method === 'optimizer.pareto') {
247
+ return [
248
+ { v1: 0.982, v2: 0.914, v3: 0.043, v4: 0.876, v5: 0.903, v6: 16, v7: 82.5, v8: 138.4, v9: 4, v10: 46.2, score: 0.926 },
249
+ { v1: 0.967, v2: 0.943, v3: 0.051, v4: 0.891, v5: 0.881, v6: 15, v7: 79.8, v8: 142.6, v9: 5, v10: 44.7, score: 0.918 },
250
+ { v1: 0.951, v2: 0.927, v3: 0.038, v4: 0.862, v5: 0.914, v6: 17, v7: 84.1, v8: 135.2, v9: 4, v10: 47.3, score: 0.923 },
251
+ ]
252
+ }
253
+
254
+ return {
255
+ method: request.method,
256
+ params: request.params ?? {},
257
+ summary: 'mock decision result',
258
+ }
259
+ }
260
+
261
+ export const ontology = async (
262
+ request: PluginOntologyPayload,
263
+ context: { plugin: PluginListItem; permission: OntologyPermission }
264
+ ) => {
265
+ console.info('[tool-plugins-preview:ontology]', context.plugin.id, request, context.permission)
266
+
267
+ if (ontologyMode === 'remote') {
268
+ if (request.action === 'executeCypher') {
269
+ return postJson(buildOntologyApiUrl(ontologyCypherQueryPath), {
270
+ cypher: request.cypher,
271
+ params: request.params,
272
+ limit: request.limit,
273
+ })
274
+ }
275
+
276
+ if (request.action === 'listConcepts') {
277
+ const query = request.query ?? {}
278
+ const limit = toFiniteNumber(query.limit, 8, (value) => value > 0)
279
+ const offset = toFiniteNumber(request.query?.offset, 0, (value) => value >= 0)
280
+ const rawEntityIds = query.entity_ids ?? query.entityIds
281
+ const entityIds = Array.isArray(rawEntityIds)
282
+ ? rawEntityIds.map((value) => String(value).trim()).filter(Boolean)
283
+ : typeof rawEntityIds === 'string'
284
+ ? rawEntityIds
285
+ .split(',')
286
+ .map((value) => value.trim())
287
+ .filter(Boolean)
288
+ : undefined
289
+
290
+ return fetchJson(
291
+ buildOntologyApiUrl(buildOntologyInstancesPath(request.conceptType), {
292
+ limit,
293
+ offset,
294
+ search: typeof query.search === 'string' ? query.search : undefined,
295
+ entity_ids: entityIds?.length ? entityIds.join(',') : undefined,
296
+ })
297
+ )
298
+ }
299
+
300
+ return fetchJson(
301
+ buildOntologyApiUrl(buildOntologyInstanceDetailPath(request.conceptType, {
302
+ entityId: request.entityId ?? request.nodeId ?? undefined,
303
+ entityName: request.entityName,
304
+ }))
305
+ )
306
+ }
307
+
308
+ if (request.action === 'executeCypher') {
309
+ return {
310
+ columns: ['value'],
311
+ rows: [{ value: 'mock cypher result' }],
312
+ rowCount: 1,
313
+ }
314
+ }
315
+
316
+ if (request.action === 'listConcepts') {
317
+ const pageSize = toFiniteNumber(request.query?.limit, 8, (value) => value > 0)
318
+ const offset = toFiniteNumber(request.query?.offset, 0, (value) => value >= 0)
319
+
320
+ return {
321
+ list: [
322
+ {
323
+ id: `${request.conceptType}:demo-1`,
324
+ label: `${request.conceptType} Demo Node`,
325
+ },
326
+ ],
327
+ total: 1,
328
+ page: Math.floor(offset / pageSize) + 1,
329
+ pageSize,
330
+ }
331
+ }
332
+
333
+ return request.nodeId
334
+ ? {
335
+ id: request.nodeId,
336
+ label: `${request.conceptType} Demo Detail`,
337
+ }
338
+ : null
339
+ }
@@ -0,0 +1,60 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ color: #e2e8f0;
7
+ background-color: #020617;
8
+ font-family:
9
+ "IBM Plex Sans",
10
+ "Segoe UI",
11
+ sans-serif;
12
+ font-synthesis: none;
13
+ text-rendering: optimizeLegibility;
14
+ -webkit-font-smoothing: antialiased;
15
+ -moz-osx-font-smoothing: grayscale;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ html,
23
+ body,
24
+ #root {
25
+ min-height: 100%;
26
+ margin: 0;
27
+ }
28
+
29
+ body {
30
+ min-width: 320px;
31
+ }
32
+
33
+ button,
34
+ input,
35
+ textarea,
36
+ select {
37
+ font: inherit;
38
+ }
39
+
40
+ .scrollbar-thin {
41
+ scrollbar-width: thin;
42
+ scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
43
+ }
44
+
45
+ .scrollbar-thin::-webkit-scrollbar {
46
+ width: 6px;
47
+ }
48
+
49
+ .scrollbar-thin::-webkit-scrollbar-track {
50
+ background: transparent;
51
+ }
52
+
53
+ .scrollbar-thin::-webkit-scrollbar-thumb {
54
+ background-color: rgba(255, 255, 255, 0.15);
55
+ border-radius: 3px;
56
+ }
57
+
58
+ .scrollbar-thin::-webkit-scrollbar-thumb:hover {
59
+ background-color: rgba(255, 255, 255, 0.25);
60
+ }
@@ -0,0 +1,8 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ['./index.html', './src/**/*.{ts,tsx}', '../../../packages/plugin-host/src/**/*.{ts,tsx}'],
4
+ theme: {
5
+ extend: {},
6
+ },
7
+ plugins: [],
8
+ }
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { createPreviewServer, formatPreviewUrl, parsePreviewCliArgs } from '../dist/cli.mjs'
3
+
4
+ const argv = process.argv.slice(2)
5
+ const { command, host, pluginDir, port } = parsePreviewCliArgs(argv)
6
+
7
+ if (command !== 'dev') {
8
+ console.error(`Unsupported command: ${command}`)
9
+ console.error('Usage: zenk-plugin-preview dev [plugin-dir] [--host 0.0.0.0] [--port 4175]')
10
+ process.exit(1)
11
+ }
12
+
13
+ const server = await createPreviewServer({
14
+ host,
15
+ pluginDir,
16
+ port,
17
+ })
18
+
19
+ await server.listen()
20
+
21
+ const resolvedUrls = server.resolvedUrls?.local ?? server.resolvedUrls?.network ?? []
22
+ const url = resolvedUrls[0] ?? formatPreviewUrl(host, port)
23
+
24
+ console.log(`[zenk-plugin-preview] plugin: ${pluginDir}`)
25
+ console.log(`[zenk-plugin-preview] ready: ${url}`)
26
+
27
+ const closeServer = async () => {
28
+ await server.close()
29
+ process.exit(0)
30
+ }
31
+
32
+ process.on('SIGINT', closeServer)
33
+ process.on('SIGTERM', closeServer)
package/dist/cli.mjs ADDED
@@ -0,0 +1,386 @@
1
+ import { dirname as N, resolve as i, basename as L, extname as H } from "node:path";
2
+ import { fileURLToPath as B } from "node:url";
3
+ import { realpathSync as z, readFileSync as R, existsSync as x } from "node:fs";
4
+ import { createRequire as S } from "node:module";
5
+ import { createServer as A, normalizePath as g } from "vite";
6
+ import J from "@vitejs/plugin-react";
7
+ import F from "autoprefixer";
8
+ import X from "tailwindcss";
9
+ const D = "preview.config.json", j = "virtual:tool-plugin-preview", k = `\0${j}`, W = "internal.plugin-preview-placeholder", Y = "0.0.0-placeholder", c = (e) => typeof e == "object" && e !== null, v = (e) => typeof e == "string" && e.trim().length > 0, V = (e) => v(e) ? e.trim() : void 0, q = (e) => {
10
+ if (!x(e)) return null;
11
+ const t = JSON.parse(R(e, "utf8"));
12
+ if (!c(t))
13
+ throw new Error(`Expected a JSON object in ${e}`);
14
+ return t;
15
+ }, b = (e) => q(i(e, D)), u = (e, t) => {
16
+ let r = e;
17
+ for (const n of t) {
18
+ if (!c(r) || !(n in r)) return;
19
+ r = r[n];
20
+ }
21
+ return r;
22
+ }, $ = (e) => {
23
+ if (!v(e))
24
+ throw new Error(`Invalid proxy prefix "${String(e)}": expected a non-empty string`);
25
+ const t = e.trim();
26
+ return (t.startsWith("/") ? t : `/${t}`).replace(/\/+$/, "") || "/";
27
+ }, K = (e, t) => {
28
+ const r = $(e);
29
+ if (v(t))
30
+ return [r, { target: t.trim(), changeOrigin: !0, secure: !1 }];
31
+ if (!c(t) || !v(t.target))
32
+ throw new Error(
33
+ `Invalid proxy config for "${r}": expected a target string or an object with a "target" field`
34
+ );
35
+ return [
36
+ r,
37
+ {
38
+ target: t.target.trim(),
39
+ changeOrigin: typeof t.changeOrigin == "boolean" ? t.changeOrigin : !0,
40
+ secure: typeof t.secure == "boolean" ? t.secure : !1
41
+ }
42
+ ];
43
+ }, Q = (e) => {
44
+ if (!c(e)) return [];
45
+ const t = u(e, ["proxy"]);
46
+ return c(t) ? Object.entries(t).map(([r, n]) => K(r, n)) : [];
47
+ }, Z = (e, t) => {
48
+ if (!c(e) && !c(t)) return null;
49
+ const r = c(e) ? e : {}, n = c(t) ? t : {};
50
+ return {
51
+ ...r,
52
+ ...n,
53
+ proxy: { ...c(r.proxy) ? r.proxy : {}, ...c(n.proxy) ? n.proxy : {} },
54
+ ontology: {
55
+ ...c(r.ontology) ? r.ontology : {},
56
+ ...c(n.ontology) ? n.ontology : {}
57
+ },
58
+ decision: {
59
+ ...c(r.decision) ? r.decision : {},
60
+ ...c(n.decision) ? n.decision : {}
61
+ }
62
+ };
63
+ }, ee = (e, t) => {
64
+ const r = e.zenk?.publicPlugins, n = e.zenk?.publicPlugin;
65
+ return c(r) ? Object.entries(r).map(([o, s]) => {
66
+ if (!c(s))
67
+ throw new Error(`Invalid zenk.publicPlugins.${o} in ${t}`);
68
+ return { entryName: o, configExport: s.configExport ?? s.manifestExport, workerExport: s.workerExport };
69
+ }) : c(n) ? [{ entryName: L(String(n.workerExport ?? "worker"), H(String(n.workerExport ?? "worker"))), configExport: n.configExport ?? n.manifestExport, workerExport: n.workerExport }] : [];
70
+ }, te = (e) => e.replace(/<parent>[\s\S]*?<\/parent>/, ""), w = (e, t) => {
71
+ const r = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), n = e.match(new RegExp(`<${r}>([\\s\\S]*?)</${r}>`));
72
+ return n ? n[1].trim() : null;
73
+ }, re = (e) => {
74
+ const t = i(e, "../pom.xml");
75
+ if (!x(t)) return null;
76
+ const r = te(R(t, "utf8")), n = w(r, "properties") ?? "", o = w(n, "plugin.id") || w(r, "artifactId"), s = w(r, "version");
77
+ return o && s ? { pluginId: o, pluginVersion: s } : null;
78
+ }, ne = (e) => {
79
+ const t = [];
80
+ for (const r of e) {
81
+ const n = i(r, "package.json"), o = JSON.parse(R(n, "utf8")), s = ee(o, n);
82
+ for (const { entryName: l, configExport: a, workerExport: d } of s) {
83
+ if (typeof a != "string" || typeof d != "string")
84
+ throw new Error(`Invalid public plugin config in ${n}`);
85
+ const m = o.exports?.[a], f = o.exports?.[d];
86
+ if (typeof m != "string" || typeof f != "string")
87
+ throw new Error(`Missing exports for public plugin in ${n}`);
88
+ t.push({
89
+ entryName: l,
90
+ configExport: a,
91
+ workerExport: d,
92
+ configModulePath: i(r, m),
93
+ workerModulePath: i(r, f),
94
+ packageDir: r,
95
+ packageJson: o,
96
+ packageJsonPath: n,
97
+ packageName: L(r),
98
+ pomIdentity: re(r)
99
+ });
100
+ }
101
+ }
102
+ return t;
103
+ }, oe = N(z(B(import.meta.url))), U = i(oe, ".."), _ = i(U, "app"), h = i(U, ".."), ie = i(h, ".."), se = i(h, "plugin-host"), ce = i(h, "plugin-types"), M = (e, t, r) => {
104
+ const n = i(e, t);
105
+ return x(n) ? n : i(e, r);
106
+ }, y = (e, t) => M(se, e, t), le = (e, t) => M(ce, e, t), ae = S(import.meta.url), T = (e, t, r) => ae.resolve(e).replace(t, r), ue = T("@remote-dom/react/host", "/build/cjs/host.cjs", "/build/esm/host.mjs"), pe = T("@remote-dom/react", "/build/cjs/index.cjs", "/build/esm/index.mjs"), de = (e, t) => S(i(e, "package.json")).resolve(t), fe = (e) => {
107
+ const t = b(ie), r = b(e), n = Z(t, r);
108
+ return { workspaceConfig: t, pluginConfig: r, resolvedConfig: n };
109
+ }, ge = (e) => {
110
+ const t = Q(e);
111
+ if (t.length === 0)
112
+ throw new Error(
113
+ `Invalid preview config: "${D}" must contain at least one entry under "proxy"`
114
+ );
115
+ return Object.fromEntries(t);
116
+ }, me = (e) => (V(u(e, ["ontology", "baseUrl"])) ?? "").replace(/\/+$/, ""), Pe = (e) => $(u(e, ["ontology", "proxyPrefix"]) ?? "/__plugin-preview-ontology"), ye = (e) => (V(u(e, ["decision", "baseUrl"])) ?? "").replace(/\/+$/, ""), he = (e) => $(u(e, ["decision", "proxyPrefix"]) ?? "/__plugin-preview-decision"), Ee = (e) => {
117
+ const t = {
118
+ VITE_PLUGIN_ONTOLOGY_MODE: u(e, ["ontology", "mode"]) ?? "mock",
119
+ VITE_PLUGIN_ONTOLOGY_BASE_URL: u(e, ["ontology", "baseUrl"]),
120
+ VITE_PLUGIN_ONTOLOGY_PROXY_PREFIX: u(e, ["ontology", "proxyPrefix"]),
121
+ VITE_SERVER_ONTOLOGY_SCHEMAS_PATH: u(e, ["ontology", "schemasPath"]),
122
+ VITE_PLUGIN_DECISION_MODE: u(e, ["decision", "mode"]) ?? "mock",
123
+ VITE_PLUGIN_DECISION_BASE_URL: u(e, ["decision", "baseUrl"]),
124
+ VITE_PLUGIN_DECISION_PROXY_PREFIX: u(e, ["decision", "proxyPrefix"]),
125
+ VITE_SERVER_DECISION_INVOKE_PATH: u(e, ["decision", "invokePath"])
126
+ };
127
+ return Object.fromEntries(
128
+ Object.entries(t).filter(([r, n]) => r.startsWith("VITE_") && typeof n == "string").map(([r, n]) => [`import.meta.env.${r}`, JSON.stringify(n)])
129
+ );
130
+ }, we = (e, t) => {
131
+ const r = [], n = [];
132
+ return e.forEach((o, s) => {
133
+ t(o.packageJsonPath), t(o.configModulePath), t(o.workerModulePath);
134
+ const l = `configModule${s}`, a = `workerUrl${s}`, d = `styleUrl${s}`, m = `/@fs${g(o.configModulePath)}`, f = `/@fs${g(o.workerModulePath)}`, I = `/@fs${g(i(o.packageDir, "resources"))}`, P = i(N(o.workerModulePath), "styles.css"), E = x(P);
135
+ r.push(`import * as ${l} from '${m}'`), r.push(`import ${a} from '${f}?worker&url'`), E && (t(P), r.push(`import ${d} from '/@fs${g(P)}?url'`));
136
+ const O = E ? `[${d}]` : "plugin.manifest.styleEntryUrls ?? []";
137
+ n.push(`
138
+ ...resolvePluginModule(${l}, ${JSON.stringify(o.pomIdentity)}).map((plugin) => {
139
+ const previewPlugin = withPreviewResourceUrls(plugin, ${JSON.stringify(I)})
140
+ return {
141
+ ...previewPlugin,
142
+ manifest: {
143
+ ...previewPlugin.manifest,
144
+ workerEntryUrl: ${a},
145
+ styleEntryUrls: ${O},
146
+ },
147
+ }
148
+ })`);
149
+ }), `
150
+ ${r.join(`
151
+ `)}
152
+
153
+ const PLACEHOLDER_PLUGIN_ID = ${JSON.stringify(W)}
154
+ const PLACEHOLDER_PLUGIN_VERSION = ${JSON.stringify(Y)}
155
+
156
+ const isRecord = (value) => typeof value === 'object' && value !== null
157
+
158
+ const isPluginListItem = (value) =>
159
+ isRecord(value) &&
160
+ typeof value.id === 'string' &&
161
+ isRecord(value.manifest) &&
162
+ typeof value.manifest.id === 'string' &&
163
+ typeof value.manifest.runtime === 'string'
164
+
165
+ const isPluginSourceDefinition = (value) =>
166
+ isRecord(value) &&
167
+ isRecord(value.manifest) &&
168
+ typeof value.manifest.name === 'string' &&
169
+ typeof value.manifest.runtime === 'string'
170
+
171
+ const assertSourceManifestDoesNotOwnGeneratedFields = (definition) => {
172
+ if ('id' in definition) {
173
+ throw new Error('Plugin source definition must not define generated field "id".')
174
+ }
175
+
176
+ for (const field of ['id', 'version', 'workerEntryUrl', 'styleEntryUrls']) {
177
+ if (field in definition.manifest) {
178
+ throw new Error(\`Plugin source manifest must not define generated field "\${field}".\`)
179
+ }
180
+ }
181
+ }
182
+
183
+ const composePluginDefinition = (definition, identity) => {
184
+ if (isPluginListItem(definition)) {
185
+ const pluginId = identity?.pluginId ?? definition.id
186
+ const pluginVersion = identity?.pluginVersion ?? definition.manifest.version
187
+ return {
188
+ ...definition,
189
+ id: pluginId,
190
+ manifest: {
191
+ ...definition.manifest,
192
+ id: pluginId,
193
+ version: pluginVersion,
194
+ },
195
+ }
196
+ }
197
+
198
+ if (!isPluginSourceDefinition(definition)) return null
199
+ const pluginId = identity?.pluginId ?? PLACEHOLDER_PLUGIN_ID
200
+ const pluginVersion = identity?.pluginVersion ?? PLACEHOLDER_PLUGIN_VERSION
201
+
202
+ assertSourceManifestDoesNotOwnGeneratedFields(definition)
203
+ return {
204
+ id: pluginId,
205
+ enabled: definition.enabled,
206
+ config: definition.config,
207
+ manifest: {
208
+ ...definition.manifest,
209
+ id: pluginId,
210
+ version: pluginVersion,
211
+ workerEntryUrl: '',
212
+ },
213
+ }
214
+ }
215
+
216
+ const resolvePluginModule = (moduleExports, identity) =>
217
+ Object.values(moduleExports)
218
+ .map((definition) => composePluginDefinition(definition, identity))
219
+ .filter(Boolean)
220
+
221
+ const resolvePreviewResourceUrl = (value, resourceBaseUrl) => {
222
+ if (typeof value !== 'string') return value
223
+ if (value.startsWith('./resources/')) return resourceBaseUrl + '/' + value.slice('./resources/'.length)
224
+ if (value.startsWith('resources/')) return resourceBaseUrl + '/' + value.slice('resources/'.length)
225
+ return value
226
+ }
227
+
228
+ const rewritePreviewResourceItem = (item, resourceBaseUrl) =>
229
+ isRecord(item)
230
+ ? {
231
+ ...item,
232
+ href: resolvePreviewResourceUrl(item.href, resourceBaseUrl),
233
+ }
234
+ : item
235
+
236
+ const rewritePreviewResources = (resources, resourceBaseUrl) =>
237
+ isRecord(resources)
238
+ ? Object.fromEntries(Object.entries(resources).map(([key, value]) => [key, rewritePreviewResourceItem(value, resourceBaseUrl)]))
239
+ : resources
240
+
241
+ const withPreviewResourceUrls = (plugin, resourceBaseUrl) => {
242
+ const display = plugin.manifest.display
243
+ if (!isRecord(display)) return plugin
244
+
245
+ return {
246
+ ...plugin,
247
+ manifest: {
248
+ ...plugin.manifest,
249
+ display: {
250
+ ...display,
251
+ resources: rewritePreviewResources(display.resources, resourceBaseUrl),
252
+ },
253
+ },
254
+ }
255
+ }
256
+
257
+ export const previewPluginDir = ${JSON.stringify(g(e[0]?.packageDir ?? ""))}
258
+ export const previewPlugins = [
259
+ ${n.join(`,
260
+ `)}
261
+ ]
262
+ .filter((plugin) => plugin.enabled !== false)
263
+ .sort((a, b) => a.manifest.order - b.manifest.order)
264
+ `;
265
+ }, ve = (e) => ({
266
+ name: "tool-plugin-preview-module",
267
+ resolveId(t) {
268
+ if (t === j) return k;
269
+ },
270
+ load(t) {
271
+ if (t !== k) return null;
272
+ const r = ne([e]);
273
+ return we(r, this.addWatchFile.bind(this));
274
+ },
275
+ handleHotUpdate(t) {
276
+ const r = g(e);
277
+ if (!g(t.file).startsWith(r)) return;
278
+ const n = t.server.moduleGraph.getModuleById(k);
279
+ n && t.server.moduleGraph.invalidateModule(n), t.server.ws.send({ type: "full-reload" });
280
+ }
281
+ }), be = (e) => {
282
+ const t = [...e], r = t.shift() ?? "dev";
283
+ let n = "0.0.0.0", o = process.cwd(), s = 4175;
284
+ for (; t.length > 0; ) {
285
+ const l = t.shift();
286
+ if (!l) break;
287
+ if (l === "--host") {
288
+ n = t.shift() ?? n;
289
+ continue;
290
+ }
291
+ if (l === "--port") {
292
+ const a = Number(t.shift());
293
+ Number.isFinite(a) && a > 0 && (s = a);
294
+ continue;
295
+ }
296
+ l.startsWith("-") || (o = i(l));
297
+ }
298
+ return { command: r, host: n, pluginDir: o, port: s };
299
+ }, Ne = (e, t) => `http://${e === "0.0.0.0" ? "localhost" : e}:${t}/`, Le = async ({ host: e, pluginDir: t, port: r }) => {
300
+ const { resolvedConfig: n } = fe(t), o = ge(n), s = me(n), l = Pe(n), a = ye(n), d = he(n), m = Ee(n), f = (p) => de(t, p), I = f("preact"), P = f("preact/hooks"), E = f("preact/jsx-runtime"), O = f("preact/jsx-dev-runtime"), G = s ? {
301
+ [l]: {
302
+ target: s,
303
+ changeOrigin: !0,
304
+ secure: !1,
305
+ rewrite: (p) => p.replace(new RegExp(`^${l}`), "")
306
+ }
307
+ } : {}, C = a ? {
308
+ [d]: {
309
+ target: a,
310
+ changeOrigin: !0,
311
+ secure: !1,
312
+ rewrite: (p) => p.replace(new RegExp(`^${d}`), "")
313
+ }
314
+ } : {};
315
+ return A({
316
+ appType: "spa",
317
+ configFile: !1,
318
+ root: _,
319
+ plugins: [
320
+ ve(t),
321
+ J({
322
+ include: [/plugin-preview\/app\/src\/.*\.[jt]sx?$/, /plugin-host\/src\/.*\.[jt]sx?$/],
323
+ exclude: [/plugin-sdk\/src\/.*\.[jt]sx?$/]
324
+ })
325
+ ],
326
+ resolve: {
327
+ alias: [
328
+ { find: /^preact\/jsx-dev-runtime$/, replacement: O },
329
+ { find: /^preact\/jsx-runtime$/, replacement: E },
330
+ { find: /^preact\/hooks$/, replacement: P },
331
+ { find: /^preact$/, replacement: I },
332
+ { find: "@remote-dom/react/host", replacement: ue },
333
+ { find: "@remote-dom/react", replacement: pe },
334
+ { find: "@zenk-agent/plugin-host/RemotePluginCardHost", replacement: y("src/RemotePluginCardHost.tsx", "dist/RemotePluginCardHost.js") },
335
+ { find: "@zenk-agent/plugin-host/hostCapabilities", replacement: y("src/hostCapabilities.ts", "dist/hostCapabilities.js") },
336
+ { find: "@zenk-agent/plugin-host/loadPlugins", replacement: y("src/loadPlugins.ts", "dist/loadPlugins.js") },
337
+ { find: "@zenk-agent/plugin-host/types", replacement: y("src/types.ts", "dist/types.js") },
338
+ { find: "@zenk-agent/plugin-host", replacement: y("src/index.ts", "dist/index.js") },
339
+ { find: "@zenk-agent/plugin-types", replacement: le("src/index.ts", "dist/index.js") }
340
+ ],
341
+ dedupe: ["react", "react-dom", "preact"]
342
+ },
343
+ optimizeDeps: {
344
+ include: ["preact", "preact/hooks", "preact/jsx-runtime", "preact/jsx-dev-runtime", "react", "react/jsx-runtime", "react-dom", "@remote-dom/react/host"]
345
+ },
346
+ define: m,
347
+ css: {
348
+ postcss: {
349
+ plugins: [
350
+ X({
351
+ content: [
352
+ i(_, "index.html"),
353
+ `${i(_, "src")}/**/*.{ts,tsx}`,
354
+ `${i(h, "plugin-host/src")}/**/*.{ts,tsx}`,
355
+ `${i(t, "src")}/**/*.{ts,tsx}`
356
+ ]
357
+ }),
358
+ F()
359
+ ]
360
+ }
361
+ },
362
+ server: {
363
+ host: e,
364
+ port: r,
365
+ proxy: { ...o, ...G, ...C },
366
+ fs: { allow: [t, U, h] }
367
+ },
368
+ preview: { host: e, port: r },
369
+ build: {
370
+ rollupOptions: {
371
+ output: {
372
+ manualChunks(p) {
373
+ if (p.includes("node_modules/echarts")) return "echarts";
374
+ if (p.includes("node_modules/react") || p.includes("node_modules/react-dom") || p.includes("node_modules/@remote-dom")) return "runtime";
375
+ }
376
+ }
377
+ }
378
+ }
379
+ });
380
+ };
381
+ export {
382
+ Le as createPreviewServer,
383
+ ne as discoverPublicPluginEntriesFromPackageDirs,
384
+ Ne as formatPreviewUrl,
385
+ be as parsePreviewCliArgs
386
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@zenk-agent/plugin-preview",
3
+ "version": "0.0.24",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/cli.mjs",
7
+ "module": "./dist/cli.mjs",
8
+ "bin": {
9
+ "zenk-plugin-preview": "./bin/zenk-plugin-preview.mjs"
10
+ },
11
+ "exports": {
12
+ ".": "./dist/cli.mjs"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "dist",
17
+ "app"
18
+ ],
19
+ "publishConfig": {
20
+ "registry": "https://registry.npmjs.org/",
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@remote-dom/react": "^1.2.2",
25
+ "@vitejs/plugin-react": "^5.1.1",
26
+ "autoprefixer": "^10.4.21",
27
+ "postcss": "^8.5.6",
28
+ "react": "19.2.5",
29
+ "react-dom": "19.2.5",
30
+ "tailwindcss": "^3.4.17",
31
+ "vite": "^7.2.0",
32
+ "@zenk-agent/plugin-host": "0.0.24",
33
+ "@zenk-agent/plugin-sdk": "0.0.24",
34
+ "@zenk-agent/plugin-types": "0.0.24"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^24.10.1",
38
+ "@types/react": "^19.2.5",
39
+ "@types/react-dom": "^19.2.3",
40
+ "typescript": "~5.9.3"
41
+ },
42
+ "scripts": {
43
+ "dev": "node ./bin/zenk-plugin-preview.mjs dev",
44
+ "build": "vite build",
45
+ "lint": "echo \"plugin-preview: no lint configured yet\"",
46
+ "typecheck": "echo \"plugin-preview: no typecheck configured yet\""
47
+ }
48
+ }