@enfyra/mcp-server 0.1.43 → 0.1.44
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/lib/mcp-instructions.js +1 -1
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/platform-operation-tools.d.ts +27 -0
- package/dist/lib/platform-operation-tools.js +245 -2
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.js +2 -1
- package/dist/lib/required-knowledge.js.map +1 -1
- package/dist/lib/tool-routing.js +2 -2
- package/dist/lib/tool-routing.js.map +1 -1
- package/dist/lib/toolset-filter.js +4 -0
- package/dist/lib/toolset-filter.js.map +1 -1
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ export function buildMcpServerInstructions(apiBaseUrl, options = {}) {
|
|
|
22
22
|
'- For target sanity checks, call `get_enfyra_api_context`; do not load broad metadata only to confirm the API base.',
|
|
23
23
|
'- When the goal is clear but the tool path is not, call `discover_enfyra_workflows` with intent/risk/surface. Use `detail: "plan"` before writes, follow `primaryPath`, and treat `avoidTools` as hard negative-routing boundaries.',
|
|
24
24
|
'- Discover before deciding, then inspect narrowly. Use `inspect_table`, `inspect_route`, `inspect_feature`, `search_admin_extensions`, or `search_runtime_zone` for the exact table/route/feature/runtime artifact. Run broad discovery tools sequentially, not in parallel.',
|
|
25
|
-
'- Load examples only when needed with `get_enfyra_examples`. For OAuth use `get_enfyra_examples({ category: "oauth-setup" })`. For extension UI, call `get_extension_theme_contract` before writing or reviewing UI, use `
|
|
25
|
+
'- Load examples only when needed with `get_enfyra_examples`. For OAuth use `get_enfyra_examples({ category: "oauth-setup" })`. For extension UI, call `get_extension_theme_contract` before writing or reviewing UI, use `build_extension_*` tools for high-contract components such as drawers, modals, page shell headers/actions, permission gates, empty states, and resource lists, and call `get_theme_class_reference` only when exact classes are needed.',
|
|
26
26
|
'- Prefer the most specific business operation tool over raw metadata CRUD.',
|
|
27
27
|
'- Before any write, call `get_enfyra_required_knowledge({ scope })` with the right scope and pass returned ack keys into write tools. Choose scope by task domain:',
|
|
28
28
|
' • `schema` — table, column, relation, record CRUD, route permission, guard, field permission, column rule, cache reload, GraphQL enablement, log debug.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-instructions.js","sourceRoot":"","sources":["../../src/lib/mcp-instructions.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,UAAU;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO;QACL,cAAc,EAAE,GAAG,IAAI,UAAU;QACjC,gBAAgB,EAAE,GAAG,IAAI,iBAAiB;KAC3C,CAAC;AACJ,CAAC;AAMD,MAAM,UAAU,0BAA0B,CAAC,UAAU,EAAE,UAAiC,EAAE;IACxF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC1E,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,IAAI,CAAC;IAEvD,OAAO;QACL,eAAe;QACf,EAAE;QACF,gCAAgC,IAAI,KAAK;QACzC,wBAAwB,cAAc,YAAY,gBAAgB,KAAK;QACvE,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,EAAE;QACF,qBAAqB;QACrB,qJAAqJ;QACrJ,qHAAqH;QACrH,qOAAqO;QACrO,8QAA8Q;QAC9Q
|
|
1
|
+
{"version":3,"file":"mcp-instructions.js","sourceRoot":"","sources":["../../src/lib/mcp-instructions.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,UAAU;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO;QACL,cAAc,EAAE,GAAG,IAAI,UAAU;QACjC,gBAAgB,EAAE,GAAG,IAAI,iBAAiB;KAC3C,CAAC;AACJ,CAAC;AAMD,MAAM,UAAU,0BAA0B,CAAC,UAAU,EAAE,UAAiC,EAAE;IACxF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC1E,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,IAAI,CAAC;IAEvD,OAAO;QACL,eAAe;QACf,EAAE;QACF,gCAAgC,IAAI,KAAK;QACzC,wBAAwB,cAAc,YAAY,gBAAgB,KAAK;QACvE,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,EAAE;QACF,qBAAqB;QACrB,qJAAqJ;QACrJ,qHAAqH;QACrH,qOAAqO;QACrO,8QAA8Q;QAC9Q,mcAAmc;QACnc,4EAA4E;QAC5E,oKAAoK;QACpK,2JAA2J;QAC3J,uJAAuJ;QACvJ,8IAA8I;QAC9I,yDAAyD;QACzD,4EAA4E;QAC5E,sMAAsM;QACtM,mLAAmL;QACnL,8LAA8L;QAC9L,EAAE;QACF,0BAA0B;QAC1B,uHAAuH;QACvH,sLAAsL;QACtL,gLAAgL;QAChL,iLAAiL;QACjL,yLAAyL;QACzL,kKAAkK;QAClK,0HAA0H;QAC1H,4LAA4L;QAC5L,EAAE;QACF,+BAA+B;QAC/B,4HAA4H;QAC5H,4GAA4G;QAC5G,mIAAmI;QACnI,kJAAkJ;QAClJ,oNAAoN;QACpN,iKAAiK;QACjK,gIAAgI;QAChI,EAAE;QACF,+BAA+B;QAC/B,iIAAiI;QACjI,uJAAuJ;QACvJ,yOAAyO;QACzO,+JAA+J;QAC/J,iMAAiM;QACjM,wSAAwS;QACxS,qLAAqL;QACrL,kPAAkP;QAClP,+PAA+P;QAC/P,EAAE;QACF,4BAA4B;QAC5B,0MAA0M;QAC1M,oHAAoH;QACpH,kIAAkI;QAClI,8MAA8M;QAC9M,kNAAkN;QAClN,wLAAwL;QACxL,EAAE;QACF,uIAAuI;KACxI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -14,6 +14,31 @@ export declare function buildExtensionModalSnippet(input: any): {
|
|
|
14
14
|
warnings: string[];
|
|
15
15
|
contract: string[];
|
|
16
16
|
};
|
|
17
|
+
export declare function buildExtensionPageShellSnippet(input: any): {
|
|
18
|
+
action: string;
|
|
19
|
+
snippet: string;
|
|
20
|
+
contract: string[];
|
|
21
|
+
};
|
|
22
|
+
export declare function buildExtensionPermissionGateSnippet(input: any): {
|
|
23
|
+
action: string;
|
|
24
|
+
component: string;
|
|
25
|
+
snippet: string;
|
|
26
|
+
normalizedBodyChanges: string[];
|
|
27
|
+
warnings: string[];
|
|
28
|
+
contract: string[];
|
|
29
|
+
};
|
|
30
|
+
export declare function buildExtensionEmptyStateSnippet(input: any): {
|
|
31
|
+
action: string;
|
|
32
|
+
component: string;
|
|
33
|
+
snippet: string;
|
|
34
|
+
contract: string[];
|
|
35
|
+
};
|
|
36
|
+
export declare function buildExtensionResourceListSnippet(input: any): {
|
|
37
|
+
action: string;
|
|
38
|
+
components: string[];
|
|
39
|
+
snippet: string;
|
|
40
|
+
contract: string[];
|
|
41
|
+
};
|
|
17
42
|
export declare function reviewExtensionUiContract(code: any): {
|
|
18
43
|
action: string;
|
|
19
44
|
valid: boolean;
|
|
@@ -28,11 +53,13 @@ export declare function reviewExtensionUiContract(code: any): {
|
|
|
28
53
|
};
|
|
29
54
|
export declare function validateExtensionCodeLocally(code: any): {
|
|
30
55
|
componentCasing: string;
|
|
56
|
+
fieldWidth: string;
|
|
31
57
|
};
|
|
32
58
|
export declare function validateExtensionCode(apiUrl: any, code: any, name: any): Promise<{
|
|
33
59
|
valid: boolean;
|
|
34
60
|
localChecks: {
|
|
35
61
|
componentCasing: string;
|
|
62
|
+
fieldWidth: string;
|
|
36
63
|
};
|
|
37
64
|
extensionId: any;
|
|
38
65
|
compiledLength: any;
|
|
@@ -19,6 +19,11 @@ const AUTO_INJECTED_EXTENSION_COMPONENT_TAGS = [
|
|
|
19
19
|
'UFormField',
|
|
20
20
|
'UIcon',
|
|
21
21
|
'UInput',
|
|
22
|
+
'UInputMenu',
|
|
23
|
+
'UInputNumber',
|
|
24
|
+
'UInputTags',
|
|
25
|
+
'UInputTime',
|
|
26
|
+
'UInputDate',
|
|
22
27
|
'UModal',
|
|
23
28
|
'USelect',
|
|
24
29
|
'USelectMenu',
|
|
@@ -30,6 +35,18 @@ const AUTO_INJECTED_EXTENSION_COMPONENT_TAGS = [
|
|
|
30
35
|
'Widget',
|
|
31
36
|
];
|
|
32
37
|
const AUTO_INJECTED_EXTENSION_COMPONENT_BY_LOWERCASE = new Map(AUTO_INJECTED_EXTENSION_COMPONENT_TAGS.map((tag) => [tag.toLowerCase(), tag]));
|
|
38
|
+
const FULL_WIDTH_EXTENSION_FIELD_TAGS = [
|
|
39
|
+
'UInput',
|
|
40
|
+
'UTextarea',
|
|
41
|
+
'USelect',
|
|
42
|
+
'USelectMenu',
|
|
43
|
+
'UInputMenu',
|
|
44
|
+
'UInputNumber',
|
|
45
|
+
'UInputTags',
|
|
46
|
+
'UInputTime',
|
|
47
|
+
'UInputDate',
|
|
48
|
+
];
|
|
49
|
+
const FULL_WIDTH_EXTENSION_FIELD_PATTERN = new RegExp(`<(${FULL_WIDTH_EXTENSION_FIELD_TAGS.join('|')})(\\s[^<>]*?)(\\/?)>`, 'g');
|
|
33
50
|
function unwrapData(result) {
|
|
34
51
|
return Array.isArray(result?.data) ? result.data : [];
|
|
35
52
|
}
|
|
@@ -308,7 +325,9 @@ function quoteJsString(value) {
|
|
|
308
325
|
function normalizeVueBodySnippet(body) {
|
|
309
326
|
let code = String(body || '').trim();
|
|
310
327
|
const changes = [];
|
|
311
|
-
code = code.replace(
|
|
328
|
+
code = code.replace(FULL_WIDTH_EXTENSION_FIELD_PATTERN, (full, tag, attrs, slash) => {
|
|
329
|
+
if (/\bdata-compact\b/.test(attrs) || /\bdata-inline\b/.test(attrs))
|
|
330
|
+
return full;
|
|
312
331
|
if (/\bclass=/.test(attrs)) {
|
|
313
332
|
const nextAttrs = attrs.replace(/class="([^"]*)"/, (classMatch, classes) => {
|
|
314
333
|
if (String(classes).split(/\s+/).includes('w-full'))
|
|
@@ -331,6 +350,23 @@ function normalizeVueBodySnippet(body) {
|
|
|
331
350
|
});
|
|
332
351
|
return { code, changes: Array.from(new Set(changes)) };
|
|
333
352
|
}
|
|
353
|
+
function findMissingFullWidthFieldControls(code) {
|
|
354
|
+
const violations = [];
|
|
355
|
+
for (const template of readTemplateBlocks(code)) {
|
|
356
|
+
let match;
|
|
357
|
+
FULL_WIDTH_EXTENSION_FIELD_PATTERN.lastIndex = 0;
|
|
358
|
+
while ((match = FULL_WIDTH_EXTENSION_FIELD_PATTERN.exec(template))) {
|
|
359
|
+
const [snippet, tag, attrs] = match;
|
|
360
|
+
if (/\bdata-compact\b/.test(attrs) || /\bdata-inline\b/.test(attrs))
|
|
361
|
+
continue;
|
|
362
|
+
const classMatch = attrs.match(/\bclass="([^"]*)"/);
|
|
363
|
+
if (!classMatch || !classMatch[1].split(/\s+/).includes('w-full')) {
|
|
364
|
+
violations.push({ tag, snippet: snippet.length > 160 ? `${snippet.slice(0, 157)}...` : snippet });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return violations;
|
|
369
|
+
}
|
|
334
370
|
function indentLines(code, spaces = 2) {
|
|
335
371
|
const pad = ' '.repeat(spaces);
|
|
336
372
|
return String(code || '')
|
|
@@ -473,6 +509,140 @@ export function buildExtensionModalSnippet(input) {
|
|
|
473
509
|
],
|
|
474
510
|
};
|
|
475
511
|
}
|
|
512
|
+
function jsObjectLiteral(entries) {
|
|
513
|
+
return `{ ${entries.filter(Boolean).join(', ')} }`;
|
|
514
|
+
}
|
|
515
|
+
function buildHeaderActionLiteral(action) {
|
|
516
|
+
const entries = [
|
|
517
|
+
action.id ? `id: ${quoteJsString(action.id)}` : null,
|
|
518
|
+
action.label ? `label: ${quoteJsString(action.label)}` : null,
|
|
519
|
+
action.icon ? `icon: ${quoteJsString(action.icon)}` : null,
|
|
520
|
+
`color: ${quoteJsString(action.color || 'neutral')}`,
|
|
521
|
+
`variant: ${quoteJsString(action.variant || 'outline')}`,
|
|
522
|
+
action.loading ? `loading: ${action.loading}` : null,
|
|
523
|
+
action.disabled ? `disabled: ${action.disabled}` : null,
|
|
524
|
+
action.to ? `to: ${quoteJsString(action.to)}` : null,
|
|
525
|
+
action.onClick ? `onClick: ${action.onClick}` : null,
|
|
526
|
+
typeof action.order === 'number' ? `order: ${action.order}` : null,
|
|
527
|
+
action.side ? `side: ${quoteJsString(action.side)}` : null,
|
|
528
|
+
];
|
|
529
|
+
return jsObjectLiteral(entries);
|
|
530
|
+
}
|
|
531
|
+
export function buildExtensionPageShellSnippet(input) {
|
|
532
|
+
const title = input.titleExpression || quoteJsString(input.title || 'Untitled');
|
|
533
|
+
const headerEntries = [
|
|
534
|
+
`title: ${title}`,
|
|
535
|
+
input.description ? `description: ${quoteJsString(input.description)}` : null,
|
|
536
|
+
input.leadingIcon ? `leadingIcon: ${quoteJsString(input.leadingIcon)}` : null,
|
|
537
|
+
`gradient: ${quoteJsString(input.gradient || 'none')}`,
|
|
538
|
+
`variant: ${quoteJsString(input.variant || 'minimal')}`,
|
|
539
|
+
];
|
|
540
|
+
const actions = Array.isArray(input.headerActions) ? input.headerActions : [];
|
|
541
|
+
const lines = [
|
|
542
|
+
'const { registerPageHeader } = usePageHeaderRegistry();',
|
|
543
|
+
`registerPageHeader(${jsObjectLiteral(headerEntries)});`,
|
|
544
|
+
];
|
|
545
|
+
if (actions.length) {
|
|
546
|
+
lines.push('const { register: registerHeaderActions } = useHeaderActionRegistry();');
|
|
547
|
+
lines.push(`registerHeaderActions([\n${actions.map((action) => ` ${buildHeaderActionLiteral(action)}`).join(',\n')}\n]);`);
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
action: 'extension_page_shell_built',
|
|
551
|
+
snippet: lines.join('\n'),
|
|
552
|
+
contract: [
|
|
553
|
+
'Use usePageHeaderRegistry so the app shell renders the page header.',
|
|
554
|
+
'Use useHeaderActionRegistry for toolbar actions instead of rendering duplicate page headers or local top bars.',
|
|
555
|
+
'Use primary solid only for the main scope action; secondary actions default to neutral outline.',
|
|
556
|
+
],
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
export function buildExtensionPermissionGateSnippet(input) {
|
|
560
|
+
const normalized = normalizeVueBodySnippet(input.body || '<slot />');
|
|
561
|
+
let condition;
|
|
562
|
+
if (input.condition) {
|
|
563
|
+
condition = input.condition;
|
|
564
|
+
}
|
|
565
|
+
else if (input.route) {
|
|
566
|
+
const methods = Array.isArray(input.methods) && input.methods.length ? input.methods : ['GET'];
|
|
567
|
+
condition = `{ or: [{ route: ${quoteJsString(input.route)}, methods: [${methods.map(quoteJsString).join(', ')}] }] }`;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
condition = 'null';
|
|
571
|
+
}
|
|
572
|
+
const snippet = [
|
|
573
|
+
`<PermissionGate :condition="${condition}">`,
|
|
574
|
+
indentLines(normalized.code, 2),
|
|
575
|
+
'</PermissionGate>',
|
|
576
|
+
].join('\n');
|
|
577
|
+
return {
|
|
578
|
+
action: 'extension_permission_gate_built',
|
|
579
|
+
component: 'PermissionGate',
|
|
580
|
+
snippet,
|
|
581
|
+
normalizedBodyChanges: normalized.changes,
|
|
582
|
+
warnings: condition === 'null' ? ['No condition/route was provided. PermissionGate with null condition permits the slot.'] : [],
|
|
583
|
+
contract: [
|
|
584
|
+
'PermissionGate is only operator UX; backend route permissions and owner checks remain authoritative.',
|
|
585
|
+
'PermissionGate renders its slot directly and should not be used as a layout wrapper.',
|
|
586
|
+
],
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
export function buildExtensionEmptyStateSnippet(input) {
|
|
590
|
+
const action = input.action
|
|
591
|
+
? `\n :action="${buildFooterActionObject(input.action)}"`
|
|
592
|
+
: '';
|
|
593
|
+
return {
|
|
594
|
+
action: 'extension_empty_state_built',
|
|
595
|
+
component: 'CommonEmptyState',
|
|
596
|
+
snippet: `<CommonEmptyState\n title="${String(input.title || 'No items found').replace(/"/g, '"')}"\n description="${String(input.description || '').replace(/"/g, '"')}"\n icon="${input.icon || 'lucide:inbox'}"\n size="${input.size || 'sm'}"\n variant="${input.variant || 'naked'}"${action}\n/>`,
|
|
597
|
+
contract: [
|
|
598
|
+
'Use CommonEmptyState for app-matched empty states.',
|
|
599
|
+
'Use variant="naked" inside framed panels/lists and outline/subtle for standalone framed empty surfaces.',
|
|
600
|
+
],
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
export function buildExtensionResourceListSnippet(input) {
|
|
604
|
+
const itemsExpression = input.itemsExpression || 'items';
|
|
605
|
+
const itemName = input.itemName || 'item';
|
|
606
|
+
const keyExpression = input.keyExpression || `${itemName}.id`;
|
|
607
|
+
const titleExpression = input.titleExpression || `${itemName}.title || ${quoteJsString('Untitled')}`;
|
|
608
|
+
const descriptionExpression = input.descriptionExpression || `${itemName}.description`;
|
|
609
|
+
const iconExpression = input.iconExpression || quoteJsString(input.icon || 'lucide:file-text');
|
|
610
|
+
const onClick = input.onClick ? `\n :on-click="() => ${input.onClick}"` : '';
|
|
611
|
+
const stats = input.statsExpression ? `\n :stats="${input.statsExpression}"` : '';
|
|
612
|
+
const actions = input.actionsExpression ? `\n :actions="${input.actionsExpression}"` : '';
|
|
613
|
+
const topBadge = input.topBadgeExpression ? `\n :top-badge="${input.topBadgeExpression}"` : '';
|
|
614
|
+
const snippet = [
|
|
615
|
+
'<CommonResourceListFrame',
|
|
616
|
+
` :loading="${input.loadingExpression || 'pending'}"`,
|
|
617
|
+
` :has-items="${itemsExpression}.length > 0"`,
|
|
618
|
+
` :total="${input.totalExpression || `${itemsExpression}.length`}"`,
|
|
619
|
+
` :items-per-page="${input.itemsPerPageExpression || '0'}"`,
|
|
620
|
+
` empty-title="${String(input.emptyTitle || 'No items found').replace(/"/g, '"')}"`,
|
|
621
|
+
` empty-description="${String(input.emptyDescription || '').replace(/"/g, '"')}"`,
|
|
622
|
+
` empty-icon="${input.emptyIcon || 'lucide:inbox'}"`,
|
|
623
|
+
'>',
|
|
624
|
+
` <CommonResourceListItem`,
|
|
625
|
+
` v-for="${itemName} in ${itemsExpression}"`,
|
|
626
|
+
` :key="${keyExpression}"`,
|
|
627
|
+
` :title="${titleExpression}"`,
|
|
628
|
+
` :description="${descriptionExpression}"`,
|
|
629
|
+
` :icon="${iconExpression}"`,
|
|
630
|
+
' icon-color="primary"',
|
|
631
|
+
`${stats}${actions}${topBadge}${onClick}`,
|
|
632
|
+
' />',
|
|
633
|
+
'</CommonResourceListFrame>',
|
|
634
|
+
].join('\n');
|
|
635
|
+
return {
|
|
636
|
+
action: 'extension_resource_list_built',
|
|
637
|
+
components: ['CommonResourceListFrame', 'CommonResourceListItem'],
|
|
638
|
+
snippet,
|
|
639
|
+
contract: [
|
|
640
|
+
'Use CommonResourceListFrame and CommonResourceListItem for operational lists instead of ad hoc cards.',
|
|
641
|
+
'Keep first-load skeleton, empty state, and pagination owned by the frame.',
|
|
642
|
+
'Use explicit bounded list data and natural pagination/search outside this snippet when the domain list can grow.',
|
|
643
|
+
],
|
|
644
|
+
};
|
|
645
|
+
}
|
|
476
646
|
export function reviewExtensionUiContract(code) {
|
|
477
647
|
const source = String(code || '');
|
|
478
648
|
const issues = [];
|
|
@@ -583,6 +753,7 @@ function getExtensionThemeContract() {
|
|
|
583
753
|
'Buttons should have stable geometry: hover may change color, border, or shadow but must not move the button or resize its content. Disabled buttons keep disabled cursor/visual state.',
|
|
584
754
|
'Inputs and textareas should not add hover movement or decorative hover states; focus, invalid, disabled, and loading states must be explicit.',
|
|
585
755
|
'Inside CommonDrawer/CommonModal body forms, UInput, UTextarea, USelect, USelectMenu, and similar field controls must use class="w-full" unless the control is intentionally inline or compact.',
|
|
756
|
+
'Extension validation rejects UInput, UTextarea, USelect, USelectMenu, UInputMenu, UInputNumber, UInputTags, UInputTime, and UInputDate without class="w-full" unless marked data-compact or data-inline.',
|
|
586
757
|
'Dynamic extensions resolve UModal to the app CommonModal. Do not pass ui.content: "eapp-surface-card" or "surface-card" to UModal/CommonModal; modal content uses the app modal surface and caller ui.content should only append z-index, width, or max-width classes.',
|
|
587
758
|
'CommonModal and CommonDrawer own action-only footers through cancelAction, primaryAction, dangerAction, leadingActions, and footerHint. Pass footer button intent through those props instead of custom footer slots. cancelAction defaults to neutral outline; use dangerAction for irreversible destructive work and tone: "primary" for Keep editing in discard dialogs.',
|
|
588
759
|
'Use custom #footer content only when the footer contains real custom layout or non-button content. Every modal/drawer button should use type="button" unless it intentionally submits a form.',
|
|
@@ -863,7 +1034,12 @@ export function validateExtensionCodeLocally(code) {
|
|
|
863
1034
|
const first = violations[0];
|
|
864
1035
|
throw new Error(`Invalid extension component casing: use <${first.expected}> instead of <${first.tag}>. Enfyra/Nuxt UI auto-injected components must keep PascalCase in extension templates; lowercase tags render as unresolved DOM elements.`);
|
|
865
1036
|
}
|
|
866
|
-
|
|
1037
|
+
const missingFullWidthFields = findMissingFullWidthFieldControls(code);
|
|
1038
|
+
if (missingFullWidthFields.length) {
|
|
1039
|
+
const first = missingFullWidthFields[0];
|
|
1040
|
+
throw new Error(`Invalid extension field width: <${first.tag}> must include class="w-full" in Enfyra extensions unless it is intentionally compact with data-compact or data-inline. First offending snippet: ${first.snippet}`);
|
|
1041
|
+
}
|
|
1042
|
+
return { componentCasing: 'passed', fieldWidth: 'passed' };
|
|
867
1043
|
}
|
|
868
1044
|
export async function validateExtensionCode(apiUrl, code, name) {
|
|
869
1045
|
const localChecks = validateExtensionCodeLocally(code);
|
|
@@ -2075,6 +2251,19 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2075
2251
|
tone: z.string().optional().describe('Optional action tone when supported by the shell component.'),
|
|
2076
2252
|
onClick: z.string().describe('Raw Vue expression or function reference for the click handler, e.g. saveNote or () => (open = false).'),
|
|
2077
2253
|
});
|
|
2254
|
+
const extensionHeaderActionSchema = z.object({
|
|
2255
|
+
id: z.string().describe('Stable action id.'),
|
|
2256
|
+
label: z.string().optional().describe('Action label.'),
|
|
2257
|
+
icon: z.string().optional().describe('Icon name such as lucide:plus or lucide:refresh-cw.'),
|
|
2258
|
+
color: z.string().optional().default('neutral').describe('Nuxt UI color. Use primary only for the single main scope action; otherwise neutral.'),
|
|
2259
|
+
variant: z.string().optional().default('outline').describe('Nuxt UI variant. Use solid for the main scope action; otherwise outline/ghost.'),
|
|
2260
|
+
loading: z.string().optional().describe('Raw Vue expression/ref name for loading state.'),
|
|
2261
|
+
disabled: z.string().optional().describe('Raw Vue expression/ref name for disabled state.'),
|
|
2262
|
+
to: z.string().optional().describe('Route path for visible navigation actions.'),
|
|
2263
|
+
onClick: z.string().optional().describe('Raw Vue expression or function reference for click behavior.'),
|
|
2264
|
+
order: z.number().optional().describe('Sort order in the shell header action area.'),
|
|
2265
|
+
side: z.enum(['left', 'right']).optional().describe('Optional shell side.'),
|
|
2266
|
+
});
|
|
2078
2267
|
server.tool('validate_dynamic_script', [
|
|
2079
2268
|
'Validate Enfyra dynamic script code before saving it to any script-backed metadata record.',
|
|
2080
2269
|
'Use this before create/update of handlers, hooks, flow steps, websocket scripts, GraphQL scripts, or bootstrap scripts when the user is iterating on code.',
|
|
@@ -2187,6 +2376,60 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2187
2376
|
].join(' '), {
|
|
2188
2377
|
code: z.string().describe('Vue SFC or template snippet to review.'),
|
|
2189
2378
|
}, async ({ code }) => jsonText(reviewExtensionUiContract(code)));
|
|
2379
|
+
server.tool('build_extension_page_shell', [
|
|
2380
|
+
'Generate page-header and shell-header-action script setup code for Enfyra page extensions.',
|
|
2381
|
+
'Use this so generated page extensions register shell chrome through usePageHeaderRegistry/useHeaderActionRegistry instead of rendering duplicate local headers.',
|
|
2382
|
+
].join(' '), {
|
|
2383
|
+
title: z.string().optional().describe('Static page title.'),
|
|
2384
|
+
titleExpression: z.string().optional().describe('Raw Vue expression for a dynamic title.'),
|
|
2385
|
+
description: z.string().optional().describe('Optional page description.'),
|
|
2386
|
+
leadingIcon: z.string().optional().describe('Optional page header icon.'),
|
|
2387
|
+
gradient: z.enum(['none', 'purple', 'blue', 'cyan']).optional().default('none').describe('Generated operational extensions should usually use none.'),
|
|
2388
|
+
variant: z.enum(['default', 'minimal', 'stats-focus']).optional().default('minimal').describe('Page header variant.'),
|
|
2389
|
+
headerActions: z.array(extensionHeaderActionSchema).optional().describe('Optional shell header actions registered through useHeaderActionRegistry.'),
|
|
2390
|
+
}, async (input) => jsonText(buildExtensionPageShellSnippet(input)));
|
|
2391
|
+
server.tool('build_extension_permission_gate', [
|
|
2392
|
+
'Generate a PermissionGate wrapper snippet for Enfyra admin extension UI.',
|
|
2393
|
+
'Use this when a visible button/block/list needs operator UX gating; backend route permissions and owner checks still remain authoritative.',
|
|
2394
|
+
].join(' '), {
|
|
2395
|
+
route: z.string().optional().describe('API route path to gate against, e.g. /notes.'),
|
|
2396
|
+
methods: z.array(z.string()).optional().describe('HTTP methods for the route condition. Defaults to GET when route is provided.'),
|
|
2397
|
+
condition: z.string().optional().describe('Raw Vue permission condition expression. Overrides route/methods when provided.'),
|
|
2398
|
+
body: z.string().describe('Vue template content to render inside PermissionGate. Field controls are normalized to w-full.'),
|
|
2399
|
+
}, async (input) => jsonText(buildExtensionPermissionGateSnippet(input)));
|
|
2400
|
+
server.tool('build_extension_empty_state', [
|
|
2401
|
+
'Generate a CommonEmptyState snippet for Enfyra admin extensions.',
|
|
2402
|
+
'Use this for app-matched empty/error/no-results states instead of hand-rolled blank panels.',
|
|
2403
|
+
].join(' '), {
|
|
2404
|
+
title: z.string().optional().describe('Empty state title.'),
|
|
2405
|
+
description: z.string().optional().describe('Empty state description.'),
|
|
2406
|
+
icon: z.string().optional().describe('Icon name. Defaults to lucide:inbox.'),
|
|
2407
|
+
size: z.enum(['sm', 'md', 'lg']).optional().default('sm').describe('Empty state size.'),
|
|
2408
|
+
variant: z.enum(['outline', 'naked', 'soft', 'subtle', 'solid']).optional().default('naked').describe('Use naked inside existing panels/lists.'),
|
|
2409
|
+
action: extensionFooterActionSchema.optional().describe('Optional primary empty-state action.'),
|
|
2410
|
+
}, async (input) => jsonText(buildExtensionEmptyStateSnippet(input)));
|
|
2411
|
+
server.tool('build_extension_resource_list', [
|
|
2412
|
+
'Generate a CommonResourceListFrame/CommonResourceListItem snippet for Enfyra admin extensions.',
|
|
2413
|
+
'Use this for operational list pages so loading, empty state, pagination placement, row chrome, icons, stats, and row actions follow the app contract.',
|
|
2414
|
+
].join(' '), {
|
|
2415
|
+
itemsExpression: z.string().optional().default('items').describe('Vue expression for the row array, e.g. notes.'),
|
|
2416
|
+
itemName: z.string().optional().default('item').describe('Loop variable name.'),
|
|
2417
|
+
keyExpression: z.string().optional().describe('Vue expression for :key. Defaults to item.id.'),
|
|
2418
|
+
titleExpression: z.string().optional().describe('Vue expression for item title. Defaults to item.title || "Untitled".'),
|
|
2419
|
+
descriptionExpression: z.string().optional().describe('Vue expression for item description. Defaults to item.description.'),
|
|
2420
|
+
icon: z.string().optional().describe('Static row icon when iconExpression is omitted.'),
|
|
2421
|
+
iconExpression: z.string().optional().describe('Vue expression for row icon.'),
|
|
2422
|
+
loadingExpression: z.string().optional().default('pending').describe('Vue expression for frame loading.'),
|
|
2423
|
+
totalExpression: z.string().optional().describe('Vue expression for total rows.'),
|
|
2424
|
+
itemsPerPageExpression: z.string().optional().describe('Vue expression for items per page; use 0 to hide pagination.'),
|
|
2425
|
+
statsExpression: z.string().optional().describe('Vue expression returning ResourceListStat[] for each row.'),
|
|
2426
|
+
actionsExpression: z.string().optional().describe('Vue expression returning ResourceListAction[] for each row.'),
|
|
2427
|
+
topBadgeExpression: z.string().optional().describe('Vue expression returning a ResourceListTopBadge for each row.'),
|
|
2428
|
+
onClick: z.string().optional().describe('Raw Vue expression called for row click, e.g. openEdit(item).'),
|
|
2429
|
+
emptyTitle: z.string().optional().describe('Empty title.'),
|
|
2430
|
+
emptyDescription: z.string().optional().describe('Empty description.'),
|
|
2431
|
+
emptyIcon: z.string().optional().describe('Empty icon.'),
|
|
2432
|
+
}, async (input) => jsonText(buildExtensionResourceListSnippet(input)));
|
|
2190
2433
|
server.tool('extension_workflow', [
|
|
2191
2434
|
'Step-by-step workflow for creating or updating Enfyra admin page, global, or widget extensions.',
|
|
2192
2435
|
'Use this when an LLM is building extension UI, menu shell notifications, account panel entries, or page/menu wiring and should follow live nextSteps instead of guessing raw enfyra_extension mutations.',
|