@majkapp/plugin-kit 3.2.1 → 3.3.1
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 +91 -0
- 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 +605 -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/HOOKS.md
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# Generated Hooks
|
|
2
|
+
|
|
3
|
+
The plugin-kit auto-generates React hooks from your functions. Run `npm run build` to generate hooks in `ui/src/generated/`.
|
|
4
|
+
|
|
5
|
+
## Query Hooks (Auto-Fetch)
|
|
6
|
+
|
|
7
|
+
Query hooks automatically fetch data when the component mounts.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// From this function definition:
|
|
11
|
+
.function('health', {
|
|
12
|
+
description: 'Check plugin health status',
|
|
13
|
+
input: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {},
|
|
16
|
+
additionalProperties: false
|
|
17
|
+
},
|
|
18
|
+
output: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
status: { type: 'string' },
|
|
22
|
+
timestamp: { type: 'string' }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
handler: async (input, ctx) => {
|
|
26
|
+
return {
|
|
27
|
+
status: 'ok',
|
|
28
|
+
timestamp: new Date().toISOString()
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Generates this hook:
|
|
34
|
+
export function useHealth(
|
|
35
|
+
input?: HealthInput,
|
|
36
|
+
options?: { enabled?: boolean; refetchInterval?: number }
|
|
37
|
+
): UseQueryResult<HealthOutput>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Basic Usage
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// ui/src/components/DashboardPage.tsx
|
|
44
|
+
import { useHealth } from '../generated/hooks';
|
|
45
|
+
|
|
46
|
+
export function DashboardPage() {
|
|
47
|
+
// Auto-fetches on mount
|
|
48
|
+
const { data, error, loading, refetch } = useHealth();
|
|
49
|
+
|
|
50
|
+
if (loading) return <div>Loading...</div>;
|
|
51
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<div data-majk-id="healthStatus">{data.status}</div>
|
|
56
|
+
<button onClick={refetch} data-majk-id="refreshButton">
|
|
57
|
+
Refresh
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### With Input Parameters
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// Function with parameters
|
|
68
|
+
.function('getAnalytics', {
|
|
69
|
+
description: 'Get analytics for time period',
|
|
70
|
+
input: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
period: { type: 'string', enum: ['24h', '7d', '30d'] }
|
|
74
|
+
},
|
|
75
|
+
required: ['period']
|
|
76
|
+
},
|
|
77
|
+
output: { /* ... */ }
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Generated hook
|
|
81
|
+
export function useGetAnalytics(
|
|
82
|
+
input: GetAnalyticsInput,
|
|
83
|
+
options?: { enabled?: boolean; refetchInterval?: number }
|
|
84
|
+
): UseQueryResult<GetAnalyticsOutput>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Usage:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { useGetAnalytics } from '../generated/hooks';
|
|
91
|
+
|
|
92
|
+
export function AnalyticsPage() {
|
|
93
|
+
// Pass input as first parameter
|
|
94
|
+
const { data, loading, error } = useGetAnalytics({ period: '7d' });
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div data-majk-id="analyticsContainer">
|
|
98
|
+
{loading && <div data-majk-state="loading">Loading...</div>}
|
|
99
|
+
{!loading && data && (
|
|
100
|
+
<div data-majk-state="populated">
|
|
101
|
+
<div data-majk-id="activeUsersCount">{data.metrics.activeUsers}</div>
|
|
102
|
+
<div data-majk-id="totalSessions">{data.metrics.totalSessions}</div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Auto-Refresh
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
export function LiveDashboard() {
|
|
114
|
+
// Refetch every 30 seconds
|
|
115
|
+
const { data } = useHealth({}, {
|
|
116
|
+
refetchInterval: 30000 // milliseconds
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return <div data-majk-id="liveStatus">{data?.status}</div>;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Conditional Fetching
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
export function ConditionalData() {
|
|
127
|
+
const [fetchEnabled, setFetchEnabled] = useState(false);
|
|
128
|
+
|
|
129
|
+
// Only fetch when enabled is true
|
|
130
|
+
const { data, loading } = useGetAnalytics(
|
|
131
|
+
{ period: '7d' },
|
|
132
|
+
{ enabled: fetchEnabled }
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div>
|
|
137
|
+
<button
|
|
138
|
+
onClick={() => setFetchEnabled(true)}
|
|
139
|
+
data-majk-id="loadDataButton"
|
|
140
|
+
>
|
|
141
|
+
Load Data
|
|
142
|
+
</button>
|
|
143
|
+
{fetchEnabled && loading && <div>Loading...</div>}
|
|
144
|
+
{data && <div>{JSON.stringify(data)}</div>}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Mutation Hooks (Manual Trigger)
|
|
151
|
+
|
|
152
|
+
Mutation hooks don't auto-fetch. Call `.mutate(input)` manually.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// From this function (mutation - modifies state):
|
|
156
|
+
.function('clearEvents', {
|
|
157
|
+
description: 'Clear all logged events',
|
|
158
|
+
input: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {},
|
|
161
|
+
additionalProperties: false
|
|
162
|
+
},
|
|
163
|
+
output: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
success: { type: 'boolean' },
|
|
167
|
+
clearedCount: { type: 'number' }
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
handler: async (input, ctx) => {
|
|
171
|
+
const events = await ctx.storage.get('events') || [];
|
|
172
|
+
await ctx.storage.set('events', []);
|
|
173
|
+
return { success: true, clearedCount: events.length };
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Generates this hook:
|
|
178
|
+
export function useClearEvents(): UseMutationResult<ClearEventsInput, ClearEventsOutput>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Basic Usage
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { useClearEvents } from '../generated/hooks';
|
|
185
|
+
|
|
186
|
+
export function EventsPage() {
|
|
187
|
+
const { mutate: clearEvents, data, loading, error } = useClearEvents();
|
|
188
|
+
|
|
189
|
+
const handleClear = async () => {
|
|
190
|
+
try {
|
|
191
|
+
const result = await clearEvents({}); // Call mutate with input
|
|
192
|
+
console.log(`Cleared ${result.clearedCount} events`);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error('Failed to clear events:', err);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<button
|
|
200
|
+
onClick={handleClear}
|
|
201
|
+
disabled={loading}
|
|
202
|
+
data-majk-id="clearEventsButton"
|
|
203
|
+
data-majk-state={loading ? 'clearing' : 'idle'}
|
|
204
|
+
>
|
|
205
|
+
{loading ? 'Clearing...' : 'Clear Events'}
|
|
206
|
+
</button>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### With Input Parameters
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// Function with input
|
|
215
|
+
.function('updateSettings', {
|
|
216
|
+
description: 'Update plugin settings',
|
|
217
|
+
input: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
properties: {
|
|
220
|
+
theme: { type: 'string', enum: ['light', 'dark'] },
|
|
221
|
+
notifications: { type: 'boolean' }
|
|
222
|
+
},
|
|
223
|
+
required: ['theme']
|
|
224
|
+
},
|
|
225
|
+
output: { /* ... */ }
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Usage:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { useUpdateSettings } from '../generated/hooks';
|
|
233
|
+
|
|
234
|
+
export function SettingsPage() {
|
|
235
|
+
const { mutate: updateSettings, loading, error } = useUpdateSettings();
|
|
236
|
+
|
|
237
|
+
const handleSave = async () => {
|
|
238
|
+
try {
|
|
239
|
+
await updateSettings({
|
|
240
|
+
theme: 'dark',
|
|
241
|
+
notifications: true
|
|
242
|
+
});
|
|
243
|
+
alert('Settings saved!');
|
|
244
|
+
} catch (err) {
|
|
245
|
+
alert(`Error: ${err.message}`);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<button
|
|
251
|
+
onClick={handleSave}
|
|
252
|
+
disabled={loading}
|
|
253
|
+
data-majk-id="saveSettingsButton"
|
|
254
|
+
>
|
|
255
|
+
Save
|
|
256
|
+
</button>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Tracking Mutation State
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
export function DeleteButton({ itemId }: { itemId: string }) {
|
|
265
|
+
const { mutate: deleteItem, loading, data, error } = useDeleteItem();
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div>
|
|
269
|
+
<button
|
|
270
|
+
onClick={() => deleteItem({ id: itemId })}
|
|
271
|
+
disabled={loading}
|
|
272
|
+
data-majk-id="deleteButton"
|
|
273
|
+
data-majk-state={loading ? 'deleting' : error ? 'error' : data ? 'success' : 'idle'}
|
|
274
|
+
>
|
|
275
|
+
{loading ? 'Deleting...' : 'Delete'}
|
|
276
|
+
</button>
|
|
277
|
+
|
|
278
|
+
{error && (
|
|
279
|
+
<div data-majk-id="errorMessage" data-majk-state="error">
|
|
280
|
+
Error: {error.message}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{data?.success && (
|
|
285
|
+
<div data-majk-id="successMessage" data-majk-state="success">
|
|
286
|
+
Deleted successfully!
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## uiLog Hook Pattern
|
|
295
|
+
|
|
296
|
+
**Best practice:** Use `useUiLog` hook for logging throughout your UI.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// Define uiLog function in plugin
|
|
300
|
+
.function('uiLog', {
|
|
301
|
+
description: 'Log messages from UI for debugging',
|
|
302
|
+
input: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] },
|
|
306
|
+
component: { type: 'string' },
|
|
307
|
+
message: { type: 'string' },
|
|
308
|
+
data: { type: 'object' }
|
|
309
|
+
},
|
|
310
|
+
required: ['level', 'component', 'message']
|
|
311
|
+
},
|
|
312
|
+
output: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
properties: { success: { type: 'boolean' } }
|
|
315
|
+
},
|
|
316
|
+
handler: async (input, ctx) => {
|
|
317
|
+
ctx.logger[input.level](`[UI:${input.component}] ${input.message}`, input.data);
|
|
318
|
+
return { success: true };
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Use in every component:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { useUiLog } from '../generated/hooks';
|
|
327
|
+
import { useEffect } from 'react';
|
|
328
|
+
|
|
329
|
+
export function DashboardPage() {
|
|
330
|
+
const { mutate: uiLog } = useUiLog();
|
|
331
|
+
|
|
332
|
+
// Log lifecycle events
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
uiLog({
|
|
335
|
+
level: 'info',
|
|
336
|
+
component: 'DashboardPage',
|
|
337
|
+
message: 'Component mounted'
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return () => {
|
|
341
|
+
uiLog({
|
|
342
|
+
level: 'info',
|
|
343
|
+
component: 'DashboardPage',
|
|
344
|
+
message: 'Component unmounting'
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
}, [uiLog]);
|
|
348
|
+
|
|
349
|
+
// Log user actions
|
|
350
|
+
const handleButtonClick = () => {
|
|
351
|
+
uiLog({
|
|
352
|
+
level: 'info',
|
|
353
|
+
component: 'DashboardPage',
|
|
354
|
+
message: 'Button clicked',
|
|
355
|
+
data: { button: 'refresh', timestamp: Date.now() }
|
|
356
|
+
});
|
|
357
|
+
// ... handle click
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Log errors
|
|
361
|
+
const handleOperation = async () => {
|
|
362
|
+
try {
|
|
363
|
+
await riskyOperation();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
uiLog({
|
|
366
|
+
level: 'error',
|
|
367
|
+
component: 'DashboardPage',
|
|
368
|
+
message: 'Operation failed',
|
|
369
|
+
data: {
|
|
370
|
+
error: error.message,
|
|
371
|
+
stack: error.stack
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
return <div>...</div>;
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Hook Return Types
|
|
382
|
+
|
|
383
|
+
### UseQueryResult
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
interface UseQueryResult<T> {
|
|
387
|
+
data: T | undefined; // Function output (undefined until loaded)
|
|
388
|
+
error: Error | undefined; // Error if fetch failed
|
|
389
|
+
loading: boolean; // True while fetching
|
|
390
|
+
refetch: () => void; // Manually trigger re-fetch
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### UseMutationResult
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
interface UseMutationResult<TInput, TOutput> {
|
|
398
|
+
mutate: (input: TInput) => Promise<TOutput>; // Call to execute function
|
|
399
|
+
data: TOutput | undefined; // Last successful result
|
|
400
|
+
error: Error | undefined; // Last error
|
|
401
|
+
loading: boolean; // True while executing
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Common Patterns
|
|
406
|
+
|
|
407
|
+
### Loading States
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
export function DataDisplay() {
|
|
411
|
+
const { data, loading, error } = useGetData();
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div data-majk-id="dataContainer" data-majk-state={
|
|
415
|
+
loading ? 'loading' :
|
|
416
|
+
error ? 'error' :
|
|
417
|
+
data ? 'populated' : 'empty'
|
|
418
|
+
}>
|
|
419
|
+
{loading && <Spinner data-majk-id="spinner" />}
|
|
420
|
+
{error && <ErrorMessage data-majk-id="error" error={error} />}
|
|
421
|
+
{data && <DataView data={data} />}
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Dependent Queries
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
export function UserProfile({ userId }: { userId: string }) {
|
|
431
|
+
// First query
|
|
432
|
+
const { data: user, loading: userLoading } = useGetUser({ id: userId });
|
|
433
|
+
|
|
434
|
+
// Second query depends on first
|
|
435
|
+
const { data: projects, loading: projectsLoading } = useGetUserProjects(
|
|
436
|
+
{ userId },
|
|
437
|
+
{ enabled: !!user } // Only fetch when user exists
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (userLoading) return <div>Loading user...</div>;
|
|
441
|
+
if (!user) return <div>User not found</div>;
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<div>
|
|
445
|
+
<div data-majk-id="userName">{user.name}</div>
|
|
446
|
+
{projectsLoading ? (
|
|
447
|
+
<div data-majk-state="loading">Loading projects...</div>
|
|
448
|
+
) : (
|
|
449
|
+
<div data-majk-state="populated">
|
|
450
|
+
{projects?.map(p => <div key={p.id}>{p.name}</div>)}
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Optimistic Updates
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
export function TodoList() {
|
|
462
|
+
const { data: todos, refetch } = useGetTodos();
|
|
463
|
+
const { mutate: completeTodo } = useCompleteTodo();
|
|
464
|
+
|
|
465
|
+
const handleComplete = async (todoId: string) => {
|
|
466
|
+
// Optimistically update UI
|
|
467
|
+
const optimisticTodos = todos?.map(t =>
|
|
468
|
+
t.id === todoId ? { ...t, completed: true } : t
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Update backend
|
|
472
|
+
try {
|
|
473
|
+
await completeTodo({ id: todoId });
|
|
474
|
+
refetch(); // Refetch to get source of truth
|
|
475
|
+
} catch (error) {
|
|
476
|
+
// Revert on error (or refetch)
|
|
477
|
+
refetch();
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
return <div>...</div>;
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Testing with Generated Hooks
|
|
486
|
+
|
|
487
|
+
Hooks are tested through UI tests:
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// tests/plugin/ui/unit/dashboard.test.js
|
|
491
|
+
import { test, bot, mock } from '@majkapp/plugin-test';
|
|
492
|
+
|
|
493
|
+
test('dashboard uses hooks correctly', async () => {
|
|
494
|
+
const context = mock()
|
|
495
|
+
.withMockHandler('health', async () => ({
|
|
496
|
+
status: 'ok',
|
|
497
|
+
timestamp: new Date().toISOString()
|
|
498
|
+
}))
|
|
499
|
+
.build();
|
|
500
|
+
|
|
501
|
+
const b = await bot(context);
|
|
502
|
+
await b.goto('/plugin-screens/my-plugin/dashboard');
|
|
503
|
+
|
|
504
|
+
// Wait for useHealth() to fetch and render
|
|
505
|
+
await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');
|
|
506
|
+
|
|
507
|
+
const text = await b.getText('[data-majk-id="healthStatus"]');
|
|
508
|
+
assert.ok(text.includes('ok'));
|
|
509
|
+
|
|
510
|
+
await b.close();
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Hook Generation
|
|
515
|
+
|
|
516
|
+
Hooks are generated by running your build:
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
npm run build
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Generated files (DO NOT EDIT manually):
|
|
523
|
+
- `ui/src/generated/hooks.ts` - React hooks
|
|
524
|
+
- `ui/src/generated/client.ts` - RPC client
|
|
525
|
+
- `ui/src/generated/types.ts` - TypeScript types
|
|
526
|
+
- `ui/src/generated/index.ts` - Barrel export
|
|
527
|
+
|
|
528
|
+
## Next Steps
|
|
529
|
+
|
|
530
|
+
Run `npx @majkapp/plugin-kit --context` - ctx API for accessing MAJK
|
|
531
|
+
Run `npx @majkapp/plugin-kit --testing` - Test your hooks
|
|
532
|
+
Run `npx @majkapp/plugin-kit --config` - Build configuration
|