@enfyra/mcp-server 0.1.47 → 0.1.48
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 +14 -0
- package/dist/lib/platform-operation-tools.js +199 -7
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.js +3 -2
- 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/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 `build_extension_ui` after the extension acknowledgement for high-contract components such as drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, FormEditor, Widget, menu/account panel registries, tabs, upload modals, and review, and call `get_theme_class_reference` only when exact classes are needed.',
|
|
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_ui` after the extension acknowledgement for high-contract components and theme decisions such as drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, FormEditor, Widget, menu/account panel registries, tabs, upload modals, theme classes/review, and full review, 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,imBAAimB;QACjmB,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"}
|
|
@@ -88,16 +88,30 @@ export declare function reviewExtensionUiContract(code: any): {
|
|
|
88
88
|
}[];
|
|
89
89
|
nextSteps: string[];
|
|
90
90
|
};
|
|
91
|
+
export declare function reviewExtensionThemeContract(code: any): {
|
|
92
|
+
action: string;
|
|
93
|
+
valid: boolean;
|
|
94
|
+
issueCount: number;
|
|
95
|
+
issues: {
|
|
96
|
+
severity: "error" | "warning";
|
|
97
|
+
rule: string;
|
|
98
|
+
message: string;
|
|
99
|
+
suggestion: string;
|
|
100
|
+
}[];
|
|
101
|
+
nextSteps: string[];
|
|
102
|
+
};
|
|
91
103
|
export declare function buildExtensionUiSnippet(kind: string, input?: AnyRecord): any;
|
|
92
104
|
export declare function validateExtensionCodeLocally(code: any): {
|
|
93
105
|
componentCasing: string;
|
|
94
106
|
fieldWidth: string;
|
|
107
|
+
themeContract: string;
|
|
95
108
|
};
|
|
96
109
|
export declare function validateExtensionCode(apiUrl: any, code: any, name: any): Promise<{
|
|
97
110
|
valid: boolean;
|
|
98
111
|
localChecks: {
|
|
99
112
|
componentCasing: string;
|
|
100
113
|
fieldWidth: string;
|
|
114
|
+
themeContract: string;
|
|
101
115
|
};
|
|
102
116
|
extensionId: any;
|
|
103
117
|
compiledLength: any;
|
|
@@ -881,6 +881,174 @@ export function reviewExtensionUiContract(code) {
|
|
|
881
881
|
: ['Snippet matches the checked modal/drawer contract rules. Still validate the final SFC before saving.'],
|
|
882
882
|
};
|
|
883
883
|
}
|
|
884
|
+
const THEME_CLASS_INTENTS = {
|
|
885
|
+
neutral_surface: {
|
|
886
|
+
classes: 'eapp-surface-card eapp-radius-panel border eapp-divider',
|
|
887
|
+
use: 'Ordinary cards, panels, KPI containers, list containers, detail blocks, and status blocks that should stay neutral.',
|
|
888
|
+
},
|
|
889
|
+
muted_surface: {
|
|
890
|
+
classes: 'eapp-surface-muted eapp-radius-panel',
|
|
891
|
+
use: 'Recessed areas, tracks, secondary panels, or subdued containers.',
|
|
892
|
+
},
|
|
893
|
+
flat_surface: {
|
|
894
|
+
classes: 'eapp-surface-flat',
|
|
895
|
+
use: 'Flush content areas that should follow the app surface without card chrome.',
|
|
896
|
+
},
|
|
897
|
+
hover_row: {
|
|
898
|
+
classes: 'eapp-surface-hover eapp-divider',
|
|
899
|
+
use: 'Clickable rows inside lists or tables.',
|
|
900
|
+
},
|
|
901
|
+
primary_identity: {
|
|
902
|
+
classes: 'eapp-primary-surface eapp-radius-panel border',
|
|
903
|
+
use: 'A selected/current entity, active plan, active package, or the single larger block representing current identity.',
|
|
904
|
+
},
|
|
905
|
+
primary_soft_icon_tile: {
|
|
906
|
+
classes: 'eapp-primary-soft eapp-icon-tile',
|
|
907
|
+
childClasses: 'eapp-primary-text',
|
|
908
|
+
use: 'Compact runtime-primary icon tiles, selected chips, and small identity accents.',
|
|
909
|
+
},
|
|
910
|
+
primary_progress: {
|
|
911
|
+
trackClasses: 'eapp-surface-muted eapp-radius-pill',
|
|
912
|
+
fillClasses: 'eapp-primary-solid',
|
|
913
|
+
use: 'Progress or meter fill controlled by the runtime primary color.',
|
|
914
|
+
},
|
|
915
|
+
status_success: {
|
|
916
|
+
classes: 'eapp-status-success-soft eapp-status-success-text eapp-status-success-border',
|
|
917
|
+
nuxtUi: { color: 'success', variant: 'soft' },
|
|
918
|
+
use: 'Small success/healthy badges, chips, or icons only.',
|
|
919
|
+
},
|
|
920
|
+
status_warning: {
|
|
921
|
+
classes: 'eapp-status-warning-soft eapp-status-warning-text eapp-status-warning-border',
|
|
922
|
+
nuxtUi: { color: 'warning', variant: 'soft' },
|
|
923
|
+
use: 'Small warning/attention badges, chips, or icons only.',
|
|
924
|
+
},
|
|
925
|
+
status_danger: {
|
|
926
|
+
classes: 'eapp-status-danger-soft eapp-status-danger-text eapp-status-danger-border',
|
|
927
|
+
nuxtUi: { color: 'error', variant: 'soft' },
|
|
928
|
+
use: 'Small danger/error/destructive badges, chips, or icons only.',
|
|
929
|
+
},
|
|
930
|
+
status_info: {
|
|
931
|
+
classes: 'eapp-status-info-soft eapp-status-info-text eapp-status-info-border',
|
|
932
|
+
nuxtUi: { color: 'info', variant: 'soft' },
|
|
933
|
+
use: 'Small informational badges, chips, or icons only.',
|
|
934
|
+
},
|
|
935
|
+
primary_action: {
|
|
936
|
+
nuxtUi: { color: 'primary', variant: 'solid' },
|
|
937
|
+
use: 'The single main action for the current scope.',
|
|
938
|
+
},
|
|
939
|
+
secondary_action: {
|
|
940
|
+
nuxtUi: { color: 'neutral', variant: 'outline' },
|
|
941
|
+
use: 'Visible secondary actions, refresh, filters, cancel, and navigation alternatives.',
|
|
942
|
+
},
|
|
943
|
+
ghost_navigation_action: {
|
|
944
|
+
nuxtUi: { color: 'neutral', variant: 'ghost' },
|
|
945
|
+
use: 'Back/navigation/icon actions that should not compete with the primary action.',
|
|
946
|
+
},
|
|
947
|
+
danger_action: {
|
|
948
|
+
nuxtUi: { color: 'error', variant: 'solid' },
|
|
949
|
+
use: 'Final destructive actions such as Delete or Remove.',
|
|
950
|
+
},
|
|
951
|
+
divider: {
|
|
952
|
+
classes: 'eapp-divider',
|
|
953
|
+
listClasses: 'eapp-divide-y',
|
|
954
|
+
use: 'Borders and row separators in extension UI.',
|
|
955
|
+
},
|
|
956
|
+
text: {
|
|
957
|
+
primary: 'eapp-text-primary',
|
|
958
|
+
secondary: 'eapp-text-secondary',
|
|
959
|
+
tertiary: 'eapp-text-tertiary',
|
|
960
|
+
quaternary: 'eapp-text-quaternary',
|
|
961
|
+
use: 'Copy hierarchy in extension UI.',
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
function buildExtensionThemeClasses(input = {}) {
|
|
965
|
+
const intent = String(input.intent || '').trim();
|
|
966
|
+
if (!intent || !(intent in THEME_CLASS_INTENTS)) {
|
|
967
|
+
return {
|
|
968
|
+
action: 'extension_theme_classes_listed',
|
|
969
|
+
validIntents: Object.keys(THEME_CLASS_INTENTS),
|
|
970
|
+
note: 'Call again with one intent to get the exact classes/props for that theme contract.',
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
const contract = THEME_CLASS_INTENTS[intent];
|
|
974
|
+
return {
|
|
975
|
+
action: 'extension_theme_classes_built',
|
|
976
|
+
intent,
|
|
977
|
+
contract,
|
|
978
|
+
hardRules: [
|
|
979
|
+
'Do not use raw CSS variable utilities such as text-[var(...)], bg-[var(...)], or border-[var(...)] in extension templates.',
|
|
980
|
+
'Do not use hardcoded Tailwind palettes such as bg-violet-*, text-cyan-*, bg-green-*, dark:bg-zinc-*, or hex/rgb/hsl colors.',
|
|
981
|
+
'Use status classes only for compact badges/icons/short text; keep large panels neutral.',
|
|
982
|
+
],
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function collectExtensionThemeIssues(code) {
|
|
986
|
+
const source = String(code || '');
|
|
987
|
+
const issues = [];
|
|
988
|
+
const push = (severity, rule, message, suggestion) => issues.push({ severity, rule, message, suggestion });
|
|
989
|
+
const concretePalettes = [
|
|
990
|
+
'slate', 'gray', 'zinc', 'neutral', 'stone',
|
|
991
|
+
'red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo',
|
|
992
|
+
'violet', 'purple', 'fuchsia', 'pink', 'rose',
|
|
993
|
+
].join('|');
|
|
994
|
+
const paletteClassPattern = new RegExp(`(?:^|[\\s"'])(?:dark:)?(?:bg|text|border|ring|divide|from|via|to)-(${concretePalettes})-(?:\\d{2,3}|950)(?:\\/\\d+)?`, 'i');
|
|
995
|
+
const rawCssVarPattern = /\b(?:bg|text|border|ring|divide|from|via|to)-\[\s*var\(--/i;
|
|
996
|
+
const neutralSemanticClassPattern = /\b(?:bg-default|bg-muted|border-default|divide-default|text-muted|text-dimmed)\b/;
|
|
997
|
+
const concreteNuxtUiColors = [
|
|
998
|
+
'slate', 'gray', 'zinc', 'stone',
|
|
999
|
+
'red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo',
|
|
1000
|
+
'violet', 'purple', 'fuchsia', 'pink', 'rose',
|
|
1001
|
+
].join('|');
|
|
1002
|
+
const concreteNuxtColorPattern = new RegExp(`\\bcolor\\s*=\\s*["'](${concreteNuxtUiColors})["']`, 'i');
|
|
1003
|
+
if (rawCssVarPattern.test(source)) {
|
|
1004
|
+
push('error', 'raw-css-var-utility', 'Raw CSS variable utility classes are not allowed in generated extension templates.', 'Use eapp-* class tokens or Nuxt UI semantic color props by intent.');
|
|
1005
|
+
}
|
|
1006
|
+
if (paletteClassPattern.test(source)) {
|
|
1007
|
+
push('error', 'hardcoded-tailwind-palette', 'Hardcoded Tailwind palette classes are not allowed in themeable extension UI.', 'Use eapp-surface-*, eapp-primary-*, eapp-status-*, or Nuxt UI semantic colors.');
|
|
1008
|
+
}
|
|
1009
|
+
if (neutralSemanticClassPattern.test(source)) {
|
|
1010
|
+
push('error', 'nuxt-neutral-class', 'Nuxt UI neutral shortcut classes are not part of the extension theme contract.', 'Use eapp-surface-* and eapp-text-* classes instead.');
|
|
1011
|
+
}
|
|
1012
|
+
if (concreteNuxtColorPattern.test(source)) {
|
|
1013
|
+
push('error', 'concrete-nuxt-color', 'Concrete Nuxt UI palette colors are not allowed in generated extension UI.', 'Use color="primary|neutral|success|warning|error|info" by semantic intent.');
|
|
1014
|
+
}
|
|
1015
|
+
if (/#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i.test(source) || /\b(?:rgba?|hsla?|oklch|lab|lch)\s*\(/i.test(source)) {
|
|
1016
|
+
push('error', 'hardcoded-color-value', 'Hardcoded color values are not allowed in extension UI.', 'Use the Enfyra theme class contract instead of hex/rgb/hsl/oklch/lab/lch values.');
|
|
1017
|
+
}
|
|
1018
|
+
if (/\bstyle\s*=\s*["'][^"']*(?:color|background(?:-color)?|border-color)\s*:/i.test(source)) {
|
|
1019
|
+
push('error', 'inline-color-style', 'Inline color styles bypass the app theme contract.', 'Use eapp-* classes or Nuxt UI semantic props instead of inline color/background/border-color.');
|
|
1020
|
+
}
|
|
1021
|
+
if (/\bgradient\s*:\s*['"](?!none['"])[^'"]+['"]/.test(source) || /\bgradient\s*=\s*['"](?!none['"])[^'"]+['"]/.test(source)) {
|
|
1022
|
+
push('error', 'page-header-gradient', 'Generated operational extension pages must use PageHeader gradient "none" unless the user explicitly requests decoration.', 'Set gradient: "none" in usePageHeaderRegistry or omit decorative gradients.');
|
|
1023
|
+
}
|
|
1024
|
+
if (/<(?:article|section|div)\b[^>]*class=["'][^"']*(?:eapp-status-(?:success|warning|danger|info)-soft|bg-(?:success|warning|error|info)(?:\b|\/))/i.test(source)) {
|
|
1025
|
+
push('warning', 'large-status-surface', 'Status color appears on a large container.', 'Keep large panels neutral and put status color on a compact UBadge/icon/short text inside.');
|
|
1026
|
+
}
|
|
1027
|
+
const primaryActionCount = (source.match(/\bcolor\s*=\s*["']primary["']/g) || []).length;
|
|
1028
|
+
if (primaryActionCount > 2) {
|
|
1029
|
+
push('warning', 'primary-overuse', 'Many primary-colored controls were found.', 'Use primary only for the main action or identity accent; use neutral variants for secondary actions.');
|
|
1030
|
+
}
|
|
1031
|
+
const primarySurfaceCount = (source.match(/\beapp-primary-surface\b/g) || []).length;
|
|
1032
|
+
if (primarySurfaceCount > 3) {
|
|
1033
|
+
push('warning', 'primary-surface-overuse', 'eapp-primary-surface appears many times.', 'Use eapp-primary-surface only for selected/current identity blocks, not every card in a list/grid.');
|
|
1034
|
+
}
|
|
1035
|
+
if (/<(?:article|section|div)\b[^>]*class=["'][^"']*\bborder\b(?![^"']*\beapp-divider\b)/i.test(source)) {
|
|
1036
|
+
push('warning', 'bare-border-token', 'A bordered panel is missing eapp-divider.', 'Pair intentional borders with eapp-divider so borders follow the app theme contract.');
|
|
1037
|
+
}
|
|
1038
|
+
return issues;
|
|
1039
|
+
}
|
|
1040
|
+
export function reviewExtensionThemeContract(code) {
|
|
1041
|
+
const issues = collectExtensionThemeIssues(code);
|
|
1042
|
+
return {
|
|
1043
|
+
action: 'extension_theme_contract_reviewed',
|
|
1044
|
+
valid: issues.every((issue) => issue.severity !== 'error'),
|
|
1045
|
+
issueCount: issues.length,
|
|
1046
|
+
issues,
|
|
1047
|
+
nextSteps: issues.length
|
|
1048
|
+
? ['Use build_extension_ui kind=theme_classes to choose classes/props by intent, then patch/update the extension.']
|
|
1049
|
+
: ['Snippet matches the checked theme contract rules. Still validate the final SFC before saving.'],
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
884
1052
|
export function buildExtensionUiSnippet(kind, input = {}) {
|
|
885
1053
|
let result;
|
|
886
1054
|
switch (kind) {
|
|
@@ -920,11 +1088,28 @@ export function buildExtensionUiSnippet(kind, input = {}) {
|
|
|
920
1088
|
case 'upload_modal':
|
|
921
1089
|
result = buildExtensionUploadModalSnippet(input);
|
|
922
1090
|
break;
|
|
1091
|
+
case 'theme_classes':
|
|
1092
|
+
result = buildExtensionThemeClasses(input);
|
|
1093
|
+
break;
|
|
1094
|
+
case 'theme_review':
|
|
1095
|
+
if (!input?.code) {
|
|
1096
|
+
throw new Error('build_extension_ui kind=theme_review requires input.code.');
|
|
1097
|
+
}
|
|
1098
|
+
result = reviewExtensionThemeContract(input.code);
|
|
1099
|
+
break;
|
|
923
1100
|
case 'review':
|
|
924
1101
|
if (!input?.code) {
|
|
925
1102
|
throw new Error('build_extension_ui kind=review requires input.code.');
|
|
926
1103
|
}
|
|
927
|
-
|
|
1104
|
+
const uiReview = reviewExtensionUiContract(input.code);
|
|
1105
|
+
const themeReview = reviewExtensionThemeContract(input.code);
|
|
1106
|
+
result = {
|
|
1107
|
+
action: 'extension_ui_and_theme_contract_reviewed',
|
|
1108
|
+
valid: uiReview.valid && themeReview.valid,
|
|
1109
|
+
issueCount: uiReview.issueCount + themeReview.issueCount,
|
|
1110
|
+
ui: uiReview,
|
|
1111
|
+
theme: themeReview,
|
|
1112
|
+
};
|
|
928
1113
|
break;
|
|
929
1114
|
default:
|
|
930
1115
|
throw new Error(`Unsupported extension UI builder kind: ${kind}`);
|
|
@@ -1000,7 +1185,7 @@ function getExtensionThemeContract() {
|
|
|
1000
1185
|
'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.',
|
|
1001
1186
|
'Inputs and textareas should not add hover movement or decorative hover states; focus, invalid, disabled, and loading states must be explicit.',
|
|
1002
1187
|
'For drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, form editors, widgets, menu/account panel registries, tabs, and upload modals, call build_extension_ui with the matching kind after extension acknowledgement before patching raw Vue.',
|
|
1003
|
-
'Use build_extension_ui kind=review before saving generated snippets that include high-contract UI or native buttons.',
|
|
1188
|
+
'Use build_extension_ui kind=theme_classes for theme classes by intent, and kind=theme_review or kind=review before saving generated snippets that include high-contract UI, theme classes, or native buttons.',
|
|
1004
1189
|
'Extension validation rejects UInput, UTextarea, USelect, USelectMenu, UInputMenu, UInputNumber, UInputTags, UInputTime, and UInputDate without class="w-full" unless marked data-compact or data-inline.',
|
|
1005
1190
|
'Use UBadge or token-backed badge spans for status. Keep badges legible in both themes with tokenized background, text, and border.',
|
|
1006
1191
|
],
|
|
@@ -1275,7 +1460,12 @@ export function validateExtensionCodeLocally(code) {
|
|
|
1275
1460
|
const first = missingFullWidthFields[0];
|
|
1276
1461
|
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}`);
|
|
1277
1462
|
}
|
|
1278
|
-
|
|
1463
|
+
const themeReview = reviewExtensionThemeContract(code);
|
|
1464
|
+
const firstThemeError = themeReview.issues.find((issue) => issue.severity === 'error');
|
|
1465
|
+
if (firstThemeError) {
|
|
1466
|
+
throw new Error(`Invalid extension theme contract: ${firstThemeError.message} Rule: ${firstThemeError.rule}. ${firstThemeError.suggestion}`);
|
|
1467
|
+
}
|
|
1468
|
+
return { componentCasing: 'passed', fieldWidth: 'passed', themeContract: 'passed' };
|
|
1279
1469
|
}
|
|
1280
1470
|
export async function validateExtensionCode(apiUrl, code, name) {
|
|
1281
1471
|
const localChecks = validateExtensionCodeLocally(code);
|
|
@@ -2464,8 +2654,8 @@ async function runExtensionWorkflow(apiUrl, opts) {
|
|
|
2464
2654
|
'Call get_extension_theme_contract before generating or reviewing extension UI.',
|
|
2465
2655
|
'useApi returns refs { data, error, pending, status } plus execute/refresh; it does not auto-run. Pass query/body/filter/deep/aggregate as objects or computed objects, not JSON strings.',
|
|
2466
2656
|
'useNotify exposes success/error/warning/info(title, description?) async helpers; do not pass Nuxt toast object payloads to it.',
|
|
2467
|
-
'For high-contract UI, call build_extension_ui after extension acknowledgement before patching raw Vue: drawer, modal, page shell, permission gate, empty state, resource list, form editor, widget, menu notification, account panel item, tabs, upload modal, or review.',
|
|
2468
|
-
'Use build_extension_ui kind=review before saving generated snippets that include drawers, modals, fields, lists, tabs, upload modals, shell registry code, or native buttons.',
|
|
2657
|
+
'For high-contract UI, call build_extension_ui after extension acknowledgement before patching raw Vue: drawer, modal, page shell, permission gate, empty state, resource list, form editor, widget, menu notification, account panel item, tabs, upload modal, theme classes, theme review, or full review.',
|
|
2658
|
+
'Use build_extension_ui kind=theme_classes for theme classes by intent, and kind=theme_review or kind=review before saving generated snippets that include drawers, modals, fields, lists, tabs, upload modals, shell registry code, theme classes, or native buttons.',
|
|
2469
2659
|
'Extension validation rejects common field controls without class="w-full" unless intentionally marked data-compact or data-inline.',
|
|
2470
2660
|
'PermissionGate renders the permitted slot directly and is UX-only; backend permissions and owner checks remain authoritative.',
|
|
2471
2661
|
'For menu/account-panel notifications, use counts only when the signal source already owns an exact count; otherwise use a dot/chip for new attention.',
|
|
@@ -2575,7 +2765,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2575
2765
|
server.tool('build_extension_ui', [
|
|
2576
2766
|
'Lazy gateway for Enfyra admin extension UI builders.',
|
|
2577
2767
|
'Use this after get_enfyra_required_knowledge(scope="extension") when a high-contract extension UI snippet is needed.',
|
|
2578
|
-
'It keeps guided startup small by dispatching drawer, modal, page_shell, permission_gate, empty_state, resource_list, form_editor, widget, menu_notification, account_panel_item, tabs, upload_modal, or review internally instead of exposing every builder tool up front.',
|
|
2768
|
+
'It keeps guided startup small by dispatching drawer, modal, page_shell, permission_gate, empty_state, resource_list, form_editor, widget, menu_notification, account_panel_item, tabs, upload_modal, theme_classes, theme_review, or review internally instead of exposing every builder tool up front.',
|
|
2579
2769
|
].join(' '), {
|
|
2580
2770
|
kind: z.enum([
|
|
2581
2771
|
'drawer',
|
|
@@ -2590,9 +2780,11 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2590
2780
|
'account_panel_item',
|
|
2591
2781
|
'tabs',
|
|
2592
2782
|
'upload_modal',
|
|
2783
|
+
'theme_classes',
|
|
2784
|
+
'theme_review',
|
|
2593
2785
|
'review',
|
|
2594
2786
|
]).describe('Which extension UI contract builder/reviewer to run.'),
|
|
2595
|
-
input: z.record(z.any()).optional().default({}).describe('Builder input object. For kind=review, pass { code }.'),
|
|
2787
|
+
input: z.record(z.any()).optional().default({}).describe('Builder input object. For kind=theme_classes, pass { intent }. For kind=theme_review/review, pass { code }.'),
|
|
2596
2788
|
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
|
|
2597
2789
|
}, async ({ kind, input, extensionKnowledgeAckKey }) => {
|
|
2598
2790
|
assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
|