@majkapp/plugin-kit 3.2.0 → 3.3.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.
- package/bin/promptable-cli.js +35 -0
- package/dist/generator/generator.js +12 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/plugin-kit.d.ts +34 -1
- package/dist/plugin-kit.d.ts.map +1 -1
- package/dist/plugin-kit.js +87 -1
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/API.md +394 -0
- package/docs/CONFIG.md +428 -0
- package/docs/CONTEXT.md +500 -0
- package/docs/FULL.md +848 -0
- package/docs/FUNCTIONS.md +623 -0
- package/docs/HOOKS.md +532 -0
- package/docs/INDEX.md +486 -0
- package/docs/LIFECYCLE.md +490 -0
- package/docs/SCREENS.md +547 -0
- package/docs/SERVICES.md +350 -0
- package/docs/TESTING.md +593 -0
- package/docs/mcp-execution-api.md +490 -0
- package/package.json +18 -3
package/docs/FULL.md
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
# Complete Reference
|
|
2
|
+
|
|
3
|
+
Comprehensive reference for @majkapp/plugin-kit covering all features and patterns.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Quick Start](#quick-start)
|
|
8
|
+
2. [Plugin Definition](#plugin-definition)
|
|
9
|
+
3. [Functions](#functions)
|
|
10
|
+
4. [Screens](#screens)
|
|
11
|
+
5. [Generated Hooks](#generated-hooks)
|
|
12
|
+
6. [Context API](#context-api)
|
|
13
|
+
7. [Services](#services)
|
|
14
|
+
8. [Lifecycle](#lifecycle)
|
|
15
|
+
9. [Testing](#testing)
|
|
16
|
+
10. [Configuration](#configuration)
|
|
17
|
+
11. [Best Practices](#best-practices)
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/index.ts
|
|
23
|
+
import { definePlugin } from '@majkapp/plugin-kit';
|
|
24
|
+
|
|
25
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
|
|
26
|
+
.pluginRoot(__dirname)
|
|
27
|
+
.function('health', {
|
|
28
|
+
description: 'Check plugin health',
|
|
29
|
+
input: { type: 'object', properties: {}, additionalProperties: false },
|
|
30
|
+
output: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
status: { type: 'string' },
|
|
34
|
+
timestamp: { type: 'string' }
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
handler: async (_, ctx) => ({
|
|
38
|
+
status: 'ok',
|
|
39
|
+
timestamp: new Date().toISOString()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
.screenReact({
|
|
43
|
+
id: 'my-plugin-main',
|
|
44
|
+
name: 'My Plugin',
|
|
45
|
+
description: 'Main screen',
|
|
46
|
+
route: '/plugin-screens/my-plugin/main',
|
|
47
|
+
pluginPath: '/index.html'
|
|
48
|
+
})
|
|
49
|
+
.build();
|
|
50
|
+
|
|
51
|
+
export = plugin;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Plugin Definition
|
|
55
|
+
|
|
56
|
+
### definePlugin()
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
definePlugin(id: string, name: string, version: string)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **id**: Unique identifier (lowercase, hyphens)
|
|
63
|
+
- **name**: Display name
|
|
64
|
+
- **version**: Semantic version
|
|
65
|
+
|
|
66
|
+
### .pluginRoot()
|
|
67
|
+
|
|
68
|
+
**REQUIRED.** Enables resource loading.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
.pluginRoot(__dirname)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### .build()
|
|
75
|
+
|
|
76
|
+
**REQUIRED.** Finalizes plugin definition.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
.build()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Always at the end.
|
|
83
|
+
|
|
84
|
+
## Functions
|
|
85
|
+
|
|
86
|
+
Define callable operations with typed inputs/outputs.
|
|
87
|
+
|
|
88
|
+
### Basic Function
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
.function('functionName', {
|
|
92
|
+
description: string, // REQUIRED: 2-3 sentences
|
|
93
|
+
input: JsonSchema, // REQUIRED: Input schema
|
|
94
|
+
output: JsonSchema, // REQUIRED: Output schema
|
|
95
|
+
handler: HandlerFn, // REQUIRED: Implementation
|
|
96
|
+
tags?: string[], // Optional: Organization tags
|
|
97
|
+
deprecated?: boolean // Optional: Mark deprecated
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Input/Output Schemas
|
|
102
|
+
|
|
103
|
+
JSON Schema for validation:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Simple types
|
|
107
|
+
{ type: 'string' }
|
|
108
|
+
{ type: 'number' }
|
|
109
|
+
{ type: 'boolean' }
|
|
110
|
+
|
|
111
|
+
// Enums
|
|
112
|
+
{ type: 'string', enum: ['value1', 'value2'] }
|
|
113
|
+
|
|
114
|
+
// Objects
|
|
115
|
+
{
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
name: { type: 'string' },
|
|
119
|
+
age: { type: 'number' }
|
|
120
|
+
},
|
|
121
|
+
required: ['name'],
|
|
122
|
+
additionalProperties: false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Arrays
|
|
126
|
+
{
|
|
127
|
+
type: 'array',
|
|
128
|
+
items: { type: 'string' }
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Handler Function
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
handler: async (input, ctx) => {
|
|
136
|
+
// ctx.logger - Logging
|
|
137
|
+
// ctx.storage - Key-value store
|
|
138
|
+
// ctx.majk - MAJK APIs
|
|
139
|
+
|
|
140
|
+
return output; // Must match output schema
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Example: Analytics Function
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
.function('getAnalytics', {
|
|
148
|
+
description: 'Get analytics for specified time period',
|
|
149
|
+
input: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
period: { type: 'string', enum: ['24h', '7d', '30d'] }
|
|
153
|
+
},
|
|
154
|
+
required: ['period']
|
|
155
|
+
},
|
|
156
|
+
output: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
period: { type: 'string' },
|
|
160
|
+
metrics: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
activeUsers: { type: 'number' },
|
|
164
|
+
totalSessions: { type: 'number' }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
handler: async (input, ctx) => {
|
|
170
|
+
const sessions = await ctx.storage.get('sessions') || [];
|
|
171
|
+
// Calculate metrics...
|
|
172
|
+
return { period: input.period, metrics: { /* ... */ } };
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Screens
|
|
178
|
+
|
|
179
|
+
Define UI screens loaded in MAJK.
|
|
180
|
+
|
|
181
|
+
### React Screen
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
.screenReact({
|
|
185
|
+
id: string, // Unique ID
|
|
186
|
+
name: string, // Display name
|
|
187
|
+
description: string, // 2-3 sentences
|
|
188
|
+
route: `/plugin-screens/${id}/path`, // Must start with prefix
|
|
189
|
+
pluginPath: string, // Path to built app
|
|
190
|
+
pluginPathHash?: string // Optional hash route
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Example: Multiple Screens
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
.screenReact({
|
|
198
|
+
id: 'my-plugin-dashboard',
|
|
199
|
+
name: 'Dashboard',
|
|
200
|
+
description: 'Main dashboard',
|
|
201
|
+
route: '/plugin-screens/my-plugin/dashboard',
|
|
202
|
+
pluginPath: '/index.html',
|
|
203
|
+
pluginPathHash: '#/'
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
.screenReact({
|
|
207
|
+
id: 'my-plugin-settings',
|
|
208
|
+
name: 'Settings',
|
|
209
|
+
description: 'Plugin settings',
|
|
210
|
+
route: '/plugin-screens/my-plugin/settings',
|
|
211
|
+
pluginPath: '/index.html',
|
|
212
|
+
pluginPathHash: '#/settings'
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### React App Setup
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// ui/src/App.tsx
|
|
220
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
221
|
+
|
|
222
|
+
export function App() {
|
|
223
|
+
return (
|
|
224
|
+
<BrowserRouter basename={window.__MAJK_IFRAME_BASE__}>
|
|
225
|
+
<Routes>
|
|
226
|
+
<Route path="/" element={<DashboardPage />} />
|
|
227
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
228
|
+
</Routes>
|
|
229
|
+
</BrowserRouter>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Generated Hooks
|
|
235
|
+
|
|
236
|
+
Auto-generated React hooks from functions.
|
|
237
|
+
|
|
238
|
+
### Query Hooks
|
|
239
|
+
|
|
240
|
+
Auto-fetch on mount:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const { data, error, loading, refetch } = useHealth(
|
|
244
|
+
input?,
|
|
245
|
+
options?: {
|
|
246
|
+
enabled?: boolean,
|
|
247
|
+
refetchInterval?: number
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Mutation Hooks
|
|
253
|
+
|
|
254
|
+
Call manually:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
const { mutate, data, error, loading } = useClearEvents();
|
|
258
|
+
|
|
259
|
+
await mutate(input);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Example: Using Hooks
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { useHealth, useGetAnalytics, useClearEvents } from './generated/hooks';
|
|
266
|
+
|
|
267
|
+
export function DashboardPage() {
|
|
268
|
+
// Query - auto-fetches
|
|
269
|
+
const { data: health } = useHealth();
|
|
270
|
+
const { data: analytics } = useGetAnalytics({ period: '7d' });
|
|
271
|
+
|
|
272
|
+
// Mutation - call manually
|
|
273
|
+
const { mutate: clearEvents } = useClearEvents();
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div>
|
|
277
|
+
<div data-majk-id="healthStatus">{health?.status}</div>
|
|
278
|
+
<button onClick={() => clearEvents({})}>Clear</button>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Context API
|
|
285
|
+
|
|
286
|
+
Handler context provides access to MAJK.
|
|
287
|
+
|
|
288
|
+
### ctx.logger
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
ctx.logger.debug(message, data);
|
|
292
|
+
ctx.logger.info(message, data);
|
|
293
|
+
ctx.logger.warn(message, data);
|
|
294
|
+
ctx.logger.error(message, data);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### ctx.storage
|
|
298
|
+
|
|
299
|
+
Plugin-scoped key-value store:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
await ctx.storage.get(key);
|
|
303
|
+
await ctx.storage.set(key, value);
|
|
304
|
+
await ctx.storage.has(key);
|
|
305
|
+
await ctx.storage.delete(key);
|
|
306
|
+
await ctx.storage.clear();
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### ctx.majk APIs
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// Conversations
|
|
313
|
+
await ctx.majk.conversations.list();
|
|
314
|
+
await ctx.majk.conversations.get(id);
|
|
315
|
+
|
|
316
|
+
// Todos
|
|
317
|
+
await ctx.majk.todos.list();
|
|
318
|
+
await ctx.majk.todos.get(id);
|
|
319
|
+
|
|
320
|
+
// Projects
|
|
321
|
+
await ctx.majk.projects.list();
|
|
322
|
+
await ctx.majk.projects.get(id);
|
|
323
|
+
|
|
324
|
+
// Teammates
|
|
325
|
+
await ctx.majk.teammates.list();
|
|
326
|
+
await ctx.majk.teammates.get(id);
|
|
327
|
+
|
|
328
|
+
// MCP Servers
|
|
329
|
+
await ctx.majk.mcpServers.list();
|
|
330
|
+
await ctx.majk.mcpServers.get(id);
|
|
331
|
+
|
|
332
|
+
// Event Bus (use in .onReady())
|
|
333
|
+
const subscription = await ctx.majk.eventBus.subscribeAll(handler);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Services
|
|
337
|
+
|
|
338
|
+
Group related functions together.
|
|
339
|
+
|
|
340
|
+
### Service Definition
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
.service(serviceName, {
|
|
344
|
+
type: string,
|
|
345
|
+
metadata: {
|
|
346
|
+
name: string,
|
|
347
|
+
description: string,
|
|
348
|
+
version: string
|
|
349
|
+
},
|
|
350
|
+
discoverable?: boolean
|
|
351
|
+
})
|
|
352
|
+
.withFunction(functionName, {
|
|
353
|
+
examples?: string[],
|
|
354
|
+
tags?: string[]
|
|
355
|
+
})
|
|
356
|
+
.endService()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Example
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
.function('health', { /* ... */, tags: ['monitoring'] })
|
|
363
|
+
.function('getStats', { /* ... */, tags: ['monitoring'] })
|
|
364
|
+
|
|
365
|
+
.service('plugin:my-plugin:monitoring', {
|
|
366
|
+
type: '@my-plugin/monitoring',
|
|
367
|
+
metadata: {
|
|
368
|
+
name: 'Monitoring Service',
|
|
369
|
+
description: 'Health checks and statistics',
|
|
370
|
+
version: '1.0.0'
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
.withFunction('health', {
|
|
374
|
+
examples: ['Check plugin health']
|
|
375
|
+
})
|
|
376
|
+
.withFunction('getStats', {
|
|
377
|
+
examples: ['Get statistics']
|
|
378
|
+
})
|
|
379
|
+
.endService()
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Lifecycle
|
|
383
|
+
|
|
384
|
+
Initialize and clean up resources.
|
|
385
|
+
|
|
386
|
+
### .onReady()
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
.onReady(async (ctx, cleanup) => {
|
|
390
|
+
// Initialization
|
|
391
|
+
const subscription = await ctx.majk.eventBus.subscribeAll(handler);
|
|
392
|
+
|
|
393
|
+
// Register cleanup
|
|
394
|
+
cleanup(() => {
|
|
395
|
+
subscription.unsubscribe();
|
|
396
|
+
});
|
|
397
|
+
})
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Example: Event Subscription
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
const eventLog = [];
|
|
404
|
+
|
|
405
|
+
.function('getEvents', {
|
|
406
|
+
/* ... */
|
|
407
|
+
handler: async () => ({ events: eventLog })
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
.onReady(async (ctx, cleanup) => {
|
|
411
|
+
const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
|
|
412
|
+
eventLog.unshift({
|
|
413
|
+
id: `${Date.now()}`,
|
|
414
|
+
type: event.type,
|
|
415
|
+
entityType: event.entityType,
|
|
416
|
+
timestamp: new Date().toISOString()
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
if (eventLog.length > 1000) {
|
|
420
|
+
eventLog.length = 1000;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
cleanup(() => {
|
|
425
|
+
subscription.unsubscribe();
|
|
426
|
+
});
|
|
427
|
+
})
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Testing
|
|
431
|
+
|
|
432
|
+
Test functions and UI with @majkapp/plugin-test.
|
|
433
|
+
|
|
434
|
+
### Function Tests
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import { test, invoke, mock } from '@majkapp/plugin-test';
|
|
438
|
+
import assert from 'assert';
|
|
439
|
+
|
|
440
|
+
test('function test', async () => {
|
|
441
|
+
const context = mock()
|
|
442
|
+
.storage({ key: 'value' })
|
|
443
|
+
.withMajkData({
|
|
444
|
+
conversations: [{ id: 'c1', title: 'Test' }]
|
|
445
|
+
})
|
|
446
|
+
.build();
|
|
447
|
+
|
|
448
|
+
const result = await invoke('functionName', { input }, { context });
|
|
449
|
+
|
|
450
|
+
assert.strictEqual(result.expected, 'value');
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### UI Tests
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { test, bot, mock } from '@majkapp/plugin-test';
|
|
458
|
+
import assert from 'assert';
|
|
459
|
+
|
|
460
|
+
test('ui test', async () => {
|
|
461
|
+
const context = mock()
|
|
462
|
+
.withMockHandler('health', async () => ({
|
|
463
|
+
status: 'ok',
|
|
464
|
+
timestamp: new Date().toISOString()
|
|
465
|
+
}))
|
|
466
|
+
.build();
|
|
467
|
+
|
|
468
|
+
const b = await bot(context);
|
|
469
|
+
await b.goto('/plugin-screens/my-plugin/main');
|
|
470
|
+
|
|
471
|
+
await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');
|
|
472
|
+
|
|
473
|
+
const text = await b.getText('[data-majk-id="healthStatus"]');
|
|
474
|
+
assert.ok(text.includes('ok'));
|
|
475
|
+
|
|
476
|
+
await b.close();
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### data-majk-* Attributes
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
// Identify elements
|
|
484
|
+
<button data-majk-id="saveButton">Save</button>
|
|
485
|
+
|
|
486
|
+
// Track state
|
|
487
|
+
<div data-majk-id="container" data-majk-state="populated">
|
|
488
|
+
{/* content */}
|
|
489
|
+
</div>
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Configuration
|
|
493
|
+
|
|
494
|
+
### vite.config.js (EXACT FORMAT)
|
|
495
|
+
|
|
496
|
+
```javascript
|
|
497
|
+
import { defineConfig } from 'vite';
|
|
498
|
+
import react from '@vitejs/plugin-react';
|
|
499
|
+
|
|
500
|
+
export default defineConfig({
|
|
501
|
+
plugins: [react()],
|
|
502
|
+
root: 'ui',
|
|
503
|
+
base: '', // IMPORTANT: Empty string
|
|
504
|
+
server: {
|
|
505
|
+
port: 3000,
|
|
506
|
+
strictPort: false,
|
|
507
|
+
},
|
|
508
|
+
build: {
|
|
509
|
+
outDir: '../dist',
|
|
510
|
+
assetsDir: 'assets',
|
|
511
|
+
emptyOutDir: true,
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Project Structure
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
my-plugin/
|
|
520
|
+
├── src/
|
|
521
|
+
│ ├── index.ts
|
|
522
|
+
│ └── core/
|
|
523
|
+
├── ui/
|
|
524
|
+
│ ├── src/
|
|
525
|
+
│ │ ├── App.tsx
|
|
526
|
+
│ │ ├── components/
|
|
527
|
+
│ │ └── generated/
|
|
528
|
+
│ └── vite.config.js
|
|
529
|
+
├── tests/
|
|
530
|
+
│ └── plugin/
|
|
531
|
+
│ ├── functions/unit/
|
|
532
|
+
│ └── ui/unit/
|
|
533
|
+
├── dist/
|
|
534
|
+
├── package.json
|
|
535
|
+
└── tsconfig.json
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Best Practices
|
|
539
|
+
|
|
540
|
+
### 1. Decouple Business Logic
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
// src/core/analytics.ts - Pure logic
|
|
544
|
+
export function calculateAnalytics(sessions, period) {
|
|
545
|
+
// Pure computation, no plugin dependencies
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/index.ts - Plugin wrapper
|
|
549
|
+
.function('getAnalytics', {
|
|
550
|
+
/* ... */
|
|
551
|
+
handler: async (input, ctx) => {
|
|
552
|
+
const sessions = await ctx.storage.get('sessions') || [];
|
|
553
|
+
return calculateAnalytics(sessions, input.period);
|
|
554
|
+
}
|
|
555
|
+
})
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 2. Always Use data-majk-* Attributes
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
<button
|
|
562
|
+
data-majk-id="saveButton"
|
|
563
|
+
data-majk-state={saving ? 'saving' : 'idle'}
|
|
564
|
+
onClick={handleSave}
|
|
565
|
+
>
|
|
566
|
+
Save
|
|
567
|
+
</button>
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### 3. Create uiLog Function
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
.function('uiLog', {
|
|
574
|
+
description: 'Log from UI',
|
|
575
|
+
input: {
|
|
576
|
+
type: 'object',
|
|
577
|
+
properties: {
|
|
578
|
+
level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] },
|
|
579
|
+
component: { type: 'string' },
|
|
580
|
+
message: { type: 'string' },
|
|
581
|
+
data: { type: 'object' }
|
|
582
|
+
},
|
|
583
|
+
required: ['level', 'component', 'message']
|
|
584
|
+
},
|
|
585
|
+
output: {
|
|
586
|
+
type: 'object',
|
|
587
|
+
properties: { success: { type: 'boolean' } }
|
|
588
|
+
},
|
|
589
|
+
handler: async (input, ctx) => {
|
|
590
|
+
ctx.logger[input.level](`[UI:${input.component}] ${input.message}`, input.data);
|
|
591
|
+
return { success: true };
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### 4. Register Cleanup in onReady
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
.onReady(async (ctx, cleanup) => {
|
|
600
|
+
const subscription = await ctx.majk.eventBus.subscribeAll(handler);
|
|
601
|
+
|
|
602
|
+
// ALWAYS register cleanup
|
|
603
|
+
cleanup(() => {
|
|
604
|
+
subscription.unsubscribe();
|
|
605
|
+
});
|
|
606
|
+
})
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### 5. Service Interfaces for External Dependencies
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
// src/core/storage-service.ts
|
|
613
|
+
export interface StorageService {
|
|
614
|
+
save(key: string, data: any): Promise<void>;
|
|
615
|
+
load(key: string): Promise<any>;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/core/aws-storage.ts - Real implementation
|
|
619
|
+
export class AWSStorageService implements StorageService {
|
|
620
|
+
// AWS SDK implementation
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/core/mock-storage.ts - Mock for testing
|
|
624
|
+
export class MockStorageService implements StorageService {
|
|
625
|
+
// In-memory implementation for tests
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
## Complete Example
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// src/index.ts
|
|
633
|
+
import { definePlugin } from '@majkapp/plugin-kit';
|
|
634
|
+
|
|
635
|
+
const eventLog: any[] = [];
|
|
636
|
+
const maxEvents = 500;
|
|
637
|
+
|
|
638
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
|
|
639
|
+
.pluginRoot(__dirname)
|
|
640
|
+
|
|
641
|
+
// Health function
|
|
642
|
+
.function('health', {
|
|
643
|
+
description: 'Check plugin health',
|
|
644
|
+
input: {
|
|
645
|
+
type: 'object',
|
|
646
|
+
properties: {},
|
|
647
|
+
additionalProperties: false
|
|
648
|
+
},
|
|
649
|
+
output: {
|
|
650
|
+
type: 'object',
|
|
651
|
+
properties: {
|
|
652
|
+
status: { type: 'string' },
|
|
653
|
+
timestamp: { type: 'string' }
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
handler: async (_, ctx) => ({
|
|
657
|
+
status: 'ok',
|
|
658
|
+
timestamp: new Date().toISOString()
|
|
659
|
+
}),
|
|
660
|
+
tags: ['monitoring']
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
// Analytics function
|
|
664
|
+
.function('getAnalytics', {
|
|
665
|
+
description: 'Get analytics',
|
|
666
|
+
input: {
|
|
667
|
+
type: 'object',
|
|
668
|
+
properties: {
|
|
669
|
+
period: { type: 'string', enum: ['24h', '7d', '30d'] }
|
|
670
|
+
},
|
|
671
|
+
required: ['period']
|
|
672
|
+
},
|
|
673
|
+
output: {
|
|
674
|
+
type: 'object',
|
|
675
|
+
properties: {
|
|
676
|
+
period: { type: 'string' },
|
|
677
|
+
metrics: {
|
|
678
|
+
type: 'object',
|
|
679
|
+
properties: {
|
|
680
|
+
activeUsers: { type: 'number' },
|
|
681
|
+
totalSessions: { type: 'number' }
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
handler: async (input, ctx) => {
|
|
687
|
+
const sessions = await ctx.storage.get('sessions') || [];
|
|
688
|
+
// Calculate...
|
|
689
|
+
return { period: input.period, metrics: { /* ... */ } };
|
|
690
|
+
},
|
|
691
|
+
tags: ['analytics']
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
// Events functions
|
|
695
|
+
.function('getEvents', {
|
|
696
|
+
description: 'Get logged events',
|
|
697
|
+
input: {
|
|
698
|
+
type: 'object',
|
|
699
|
+
properties: {},
|
|
700
|
+
additionalProperties: false
|
|
701
|
+
},
|
|
702
|
+
output: {
|
|
703
|
+
type: 'object',
|
|
704
|
+
properties: {
|
|
705
|
+
events: { type: 'array' },
|
|
706
|
+
count: { type: 'number' }
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
handler: async () => ({
|
|
710
|
+
events: eventLog,
|
|
711
|
+
count: eventLog.length
|
|
712
|
+
}),
|
|
713
|
+
tags: ['events']
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
.function('clearEvents', {
|
|
717
|
+
description: 'Clear event log',
|
|
718
|
+
input: {
|
|
719
|
+
type: 'object',
|
|
720
|
+
properties: {},
|
|
721
|
+
additionalProperties: false
|
|
722
|
+
},
|
|
723
|
+
output: {
|
|
724
|
+
type: 'object',
|
|
725
|
+
properties: {
|
|
726
|
+
success: { type: 'boolean' },
|
|
727
|
+
clearedCount: { type: 'number' }
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
handler: async (_, ctx) => {
|
|
731
|
+
const count = eventLog.length;
|
|
732
|
+
eventLog.length = 0;
|
|
733
|
+
ctx.logger.info(`Cleared ${count} events`);
|
|
734
|
+
return { success: true, clearedCount: count };
|
|
735
|
+
},
|
|
736
|
+
tags: ['events']
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
// UI logging
|
|
740
|
+
.function('uiLog', {
|
|
741
|
+
description: 'Log from UI',
|
|
742
|
+
input: {
|
|
743
|
+
type: 'object',
|
|
744
|
+
properties: {
|
|
745
|
+
level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] },
|
|
746
|
+
component: { type: 'string' },
|
|
747
|
+
message: { type: 'string' },
|
|
748
|
+
data: { type: 'object' }
|
|
749
|
+
},
|
|
750
|
+
required: ['level', 'component', 'message']
|
|
751
|
+
},
|
|
752
|
+
output: {
|
|
753
|
+
type: 'object',
|
|
754
|
+
properties: { success: { type: 'boolean' } }
|
|
755
|
+
},
|
|
756
|
+
handler: async (input, ctx) => {
|
|
757
|
+
ctx.logger[input.level](`[UI:${input.component}] ${input.message}`, input.data);
|
|
758
|
+
return { success: true };
|
|
759
|
+
},
|
|
760
|
+
tags: ['debugging']
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
// Screens
|
|
764
|
+
.screenReact({
|
|
765
|
+
id: 'my-plugin-dashboard',
|
|
766
|
+
name: 'Dashboard',
|
|
767
|
+
description: 'Main dashboard',
|
|
768
|
+
route: '/plugin-screens/my-plugin/dashboard',
|
|
769
|
+
pluginPath: '/index.html',
|
|
770
|
+
pluginPathHash: '#/'
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
.screenReact({
|
|
774
|
+
id: 'my-plugin-events',
|
|
775
|
+
name: 'Events',
|
|
776
|
+
description: 'Event monitor',
|
|
777
|
+
route: '/plugin-screens/my-plugin/events',
|
|
778
|
+
pluginPath: '/index.html',
|
|
779
|
+
pluginPathHash: '#/events'
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
// Services
|
|
783
|
+
.service('plugin:my-plugin:monitoring', {
|
|
784
|
+
type: '@my-plugin/monitoring',
|
|
785
|
+
metadata: {
|
|
786
|
+
name: 'Monitoring Service',
|
|
787
|
+
description: 'Health checks and analytics',
|
|
788
|
+
version: '1.0.0'
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
.withFunction('health')
|
|
792
|
+
.withFunction('getAnalytics')
|
|
793
|
+
.endService()
|
|
794
|
+
|
|
795
|
+
.service('plugin:my-plugin:events', {
|
|
796
|
+
type: '@my-plugin/events',
|
|
797
|
+
metadata: {
|
|
798
|
+
name: 'Event Management',
|
|
799
|
+
description: 'Event logging and monitoring',
|
|
800
|
+
version: '1.0.0'
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
.withFunction('getEvents')
|
|
804
|
+
.withFunction('clearEvents')
|
|
805
|
+
.endService()
|
|
806
|
+
|
|
807
|
+
// Lifecycle
|
|
808
|
+
.onReady(async (ctx, cleanup) => {
|
|
809
|
+
ctx.logger.info('My Plugin initializing');
|
|
810
|
+
|
|
811
|
+
// Subscribe to events
|
|
812
|
+
const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
|
|
813
|
+
eventLog.unshift({
|
|
814
|
+
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
815
|
+
timestamp: new Date().toISOString(),
|
|
816
|
+
entityType: event.entityType,
|
|
817
|
+
eventType: event.type
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
if (eventLog.length > maxEvents) {
|
|
821
|
+
eventLog.length = maxEvents;
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
cleanup(() => {
|
|
826
|
+
subscription.unsubscribe();
|
|
827
|
+
ctx.logger.info('Event subscription cleaned up');
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
ctx.logger.info('My Plugin ready');
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
.build();
|
|
834
|
+
|
|
835
|
+
export = plugin;
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
## Next Steps
|
|
839
|
+
|
|
840
|
+
Run `npx @majkapp/plugin-kit` - Quick start
|
|
841
|
+
Run `npx @majkapp/plugin-kit --functions` - Function patterns
|
|
842
|
+
Run `npx @majkapp/plugin-kit --screens` - Screen configuration
|
|
843
|
+
Run `npx @majkapp/plugin-kit --hooks` - Generated hooks
|
|
844
|
+
Run `npx @majkapp/plugin-kit --context` - Context API
|
|
845
|
+
Run `npx @majkapp/plugin-kit --services` - Service grouping
|
|
846
|
+
Run `npx @majkapp/plugin-kit --lifecycle` - Lifecycle hooks
|
|
847
|
+
Run `npx @majkapp/plugin-kit --testing` - Testing guide
|
|
848
|
+
Run `npx @majkapp/plugin-kit --config` - Project configuration
|