@jay-framework/plugin-validator 0.20.0 → 0.22.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/dist/index.d.ts CHANGED
@@ -18,16 +18,22 @@ interface ValidationResult {
18
18
  typesGenerated?: number;
19
19
  }
20
20
  interface ValidationError {
21
- type: 'schema' | 'file-missing' | 'export-mismatch' | 'contract-invalid' | 'component-contract-mismatch' | 'type-generation-failed';
21
+ type: 'schema' | 'file-missing' | 'export-mismatch' | 'contract-invalid' | 'component-contract-mismatch' | 'type-generation-failed' | 'add-menu-catalog';
22
22
  message: string;
23
23
  location?: string;
24
24
  suggestion?: string;
25
+ /** Add Menu lint / schema code (Design Log #30) */
26
+ code?: string;
27
+ itemId?: string;
25
28
  }
26
29
  interface ValidationWarning {
27
30
  type: string;
28
31
  message: string;
29
32
  location?: string;
30
33
  suggestion?: string;
34
+ /** Add Menu lint code (Design Log #30) */
35
+ code?: string;
36
+ itemId?: string;
31
37
  }
32
38
  interface PluginContext {
33
39
  manifest: PluginManifest;
@@ -79,4 +85,72 @@ interface CheckResult {
79
85
  */
80
86
  declare function checkComponentPropsAndParams(sourceCode: string, contract: ContractPropsAndParams, contractName: string, contractPath: string, sourcePath: string): CheckResult;
81
87
 
82
- export { type PluginContext, type ValidatePluginOptions, type ValidationError, type ValidationResult, type ValidationWarning, checkComponentPropsAndParams, validatePlugin };
88
+ /**
89
+ * Add Menu catalog schema validation and lint.
90
+ *
91
+ * Source of truth for plugin authors (`jay-stack validate-plugin`) and for
92
+ * AIditor runtime catalog loading. When changing Add Menu schema or lint rules,
93
+ * update this module and `agent-kit/plugin/aiditor-add-menu.md` in the same change.
94
+ */
95
+ type AddMenuBrowseSize = 'large' | 'medium' | 'small';
96
+ type AddMenuPresentation = {
97
+ type: 'image';
98
+ src: string;
99
+ } | {
100
+ type: 'gif';
101
+ src: string;
102
+ poster?: string;
103
+ } | {
104
+ type: 'html-fragment';
105
+ html: string;
106
+ };
107
+ type AddMenuInteraction = {
108
+ mode: 'reference' | 'stage-place';
109
+ stagePromptTemplate?: string;
110
+ };
111
+ type AddMenuCatalogLintWarning = {
112
+ code: string;
113
+ message: string;
114
+ itemId?: string;
115
+ sourcePath?: string;
116
+ };
117
+ type AddMenuValidationError = {
118
+ path: string;
119
+ message: string;
120
+ code?: string;
121
+ };
122
+ type AddMenuItem = {
123
+ id: string;
124
+ title: string;
125
+ category: string;
126
+ prompt: string;
127
+ pluginName?: string;
128
+ packageName?: string;
129
+ subCategory?: string;
130
+ folderPath?: string[];
131
+ thumbnail?: string;
132
+ presentation?: AddMenuPresentation;
133
+ browse?: {
134
+ size?: AddMenuBrowseSize;
135
+ };
136
+ interaction?: AddMenuInteraction;
137
+ };
138
+ type AddMenuCatalogFile = {
139
+ items: AddMenuItem[];
140
+ };
141
+ declare function validateAddMenuItem(raw: unknown, itemPath: string): {
142
+ item: AddMenuItem | null;
143
+ errors: AddMenuValidationError[];
144
+ };
145
+ declare function validateAddMenuCatalogFile(raw: unknown, sourcePath: string): {
146
+ file: AddMenuCatalogFile | null;
147
+ errors: AddMenuValidationError[];
148
+ };
149
+ declare function lintAddMenuCatalog(items: AddMenuItem[], sourcePath: string): {
150
+ errors: AddMenuCatalogLintWarning[];
151
+ warnings: AddMenuCatalogLintWarning[];
152
+ };
153
+ /** Human-readable fix hints for validate-plugin output. */
154
+ declare const ADD_MENU_VALIDATION_SUGGESTIONS: Record<string, string>;
155
+
156
+ export { ADD_MENU_VALIDATION_SUGGESTIONS, type AddMenuBrowseSize, type AddMenuCatalogFile, type AddMenuCatalogLintWarning, type AddMenuInteraction, type AddMenuItem, type AddMenuPresentation, type AddMenuValidationError, type PluginContext, type ValidatePluginOptions, type ValidationError, type ValidationResult, type ValidationWarning, checkComponentPropsAndParams, lintAddMenuCatalog, validateAddMenuCatalogFile, validateAddMenuItem, validatePlugin };
package/dist/index.js CHANGED
@@ -272,6 +272,769 @@ function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImport
272
272
  }
273
273
  }
274
274
  }
275
+ const REJECTED_ITEM_FIELDS = ["kind", "parameters", "component", "allowedScopes"];
276
+ const HTML_SOFT_LIMIT_BYTES = 8 * 1024;
277
+ const HTML_HARD_LIMIT_BYTES = 32 * 1024;
278
+ const FOLDER_PATH_MAX_SEGMENTS = 32;
279
+ const BLOCKED_TAGS = /<\s*(script|iframe|object|embed)\b[^>]*>[\s\S]*?<\/\s*\1\s*>|<\s*(script|iframe|object|embed)\b[^>]*\/?>/gi;
280
+ const EVENT_HANDLER_ATTR = /\s+on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi;
281
+ const JAVASCRIPT_URL = /\b(href|src|xlink:href)\s*=\s*("|')\s*javascript:/gi;
282
+ function isRecord(value) {
283
+ return typeof value === "object" && value !== null && !Array.isArray(value);
284
+ }
285
+ function byteLengthUtf8(value) {
286
+ return new TextEncoder().encode(value).length;
287
+ }
288
+ function detectUnsafeAddMenuHtmlFragment(html) {
289
+ if (BLOCKED_TAGS.test(html)) {
290
+ BLOCKED_TAGS.lastIndex = 0;
291
+ return {
292
+ code: "html-fragment-unsafe-markup",
293
+ message: "html-fragment must not include script, iframe, object, or embed"
294
+ };
295
+ }
296
+ BLOCKED_TAGS.lastIndex = 0;
297
+ if (EVENT_HANDLER_ATTR.test(html)) {
298
+ EVENT_HANDLER_ATTR.lastIndex = 0;
299
+ return {
300
+ code: "html-fragment-unsafe-markup",
301
+ message: "html-fragment must not include inline event handler attributes"
302
+ };
303
+ }
304
+ EVENT_HANDLER_ATTR.lastIndex = 0;
305
+ if (JAVASCRIPT_URL.test(html)) {
306
+ JAVASCRIPT_URL.lastIndex = 0;
307
+ return {
308
+ code: "html-fragment-unsafe-markup",
309
+ message: "html-fragment must not include javascript: URLs"
310
+ };
311
+ }
312
+ JAVASCRIPT_URL.lastIndex = 0;
313
+ return null;
314
+ }
315
+ function sanitizeAddMenuHtmlFragment(html) {
316
+ let result = html;
317
+ result = result.replace(BLOCKED_TAGS, "");
318
+ BLOCKED_TAGS.lastIndex = 0;
319
+ result = result.replace(EVENT_HANDLER_ATTR, "");
320
+ EVENT_HANDLER_ATTR.lastIndex = 0;
321
+ result = result.replace(JAVASCRIPT_URL, "");
322
+ JAVASCRIPT_URL.lastIndex = 0;
323
+ return result.trim();
324
+ }
325
+ function catalogWarning(code, message, itemId, sourcePath) {
326
+ return { code, message, ...itemId ? { itemId } : {}, ...sourcePath ? { sourcePath } : {} };
327
+ }
328
+ function requiredString(obj, field, itemPath, errors, code) {
329
+ const value = obj[field];
330
+ if (typeof value !== "string" || value.trim().length === 0) {
331
+ errors.push({
332
+ path: `${itemPath}.${field}`,
333
+ message: "required non-empty string",
334
+ ...code ? { code } : {}
335
+ });
336
+ return null;
337
+ }
338
+ return value.trim();
339
+ }
340
+ function optionalString(obj, field) {
341
+ const value = obj[field];
342
+ if (value === void 0)
343
+ return void 0;
344
+ if (typeof value !== "string")
345
+ return void 0;
346
+ const trimmed = value.trim();
347
+ return trimmed.length > 0 ? trimmed : void 0;
348
+ }
349
+ function validateInteraction(raw, itemPath, errors) {
350
+ if (raw === void 0)
351
+ return void 0;
352
+ if (!isRecord(raw)) {
353
+ errors.push({
354
+ path: itemPath,
355
+ message: "interaction must be an object",
356
+ code: "interaction-not-object"
357
+ });
358
+ return void 0;
359
+ }
360
+ const mode = raw.mode;
361
+ if (mode !== "reference" && mode !== "stage-place") {
362
+ errors.push({
363
+ path: `${itemPath}.mode`,
364
+ message: 'interaction.mode must be "reference" or "stage-place"',
365
+ code: "interaction-invalid-mode"
366
+ });
367
+ return void 0;
368
+ }
369
+ return {
370
+ mode,
371
+ stagePromptTemplate: optionalString(raw, "stagePromptTemplate")
372
+ };
373
+ }
374
+ function validatePresentation(raw, itemPath, errors) {
375
+ if (raw === void 0)
376
+ return void 0;
377
+ if (!isRecord(raw)) {
378
+ errors.push({
379
+ path: itemPath,
380
+ message: "presentation must be an object",
381
+ code: "presentation-not-object"
382
+ });
383
+ return void 0;
384
+ }
385
+ const type = raw.type;
386
+ if (type !== "image" && type !== "gif" && type !== "html-fragment") {
387
+ errors.push({
388
+ path: `${itemPath}.type`,
389
+ message: 'presentation.type must be "image", "gif", or "html-fragment"',
390
+ code: "presentation-unknown-type"
391
+ });
392
+ return void 0;
393
+ }
394
+ if (type === "image") {
395
+ const src = requiredString(raw, "src", itemPath, errors, "presentation-missing-src");
396
+ if (!src)
397
+ return void 0;
398
+ return { type: "image", src };
399
+ }
400
+ if (type === "gif") {
401
+ const src = requiredString(raw, "src", itemPath, errors, "presentation-missing-src");
402
+ if (!src)
403
+ return void 0;
404
+ return {
405
+ type: "gif",
406
+ src,
407
+ poster: optionalString(raw, "poster")
408
+ };
409
+ }
410
+ if ("src" in raw) {
411
+ errors.push({
412
+ path: `${itemPath}.src`,
413
+ message: "html-fragment must use presentation.html, not presentation.src",
414
+ code: "html-fragment-src-not-allowed"
415
+ });
416
+ }
417
+ const htmlValue = raw.html;
418
+ if (typeof htmlValue !== "string" || htmlValue.trim().length === 0) {
419
+ errors.push({
420
+ path: `${itemPath}.html`,
421
+ message: "html-fragment requires non-empty presentation.html",
422
+ code: "html-fragment-missing-html"
423
+ });
424
+ return void 0;
425
+ }
426
+ const unsafe = detectUnsafeAddMenuHtmlFragment(htmlValue);
427
+ if (unsafe) {
428
+ errors.push({
429
+ path: `${itemPath}.html`,
430
+ message: unsafe.message,
431
+ code: unsafe.code
432
+ });
433
+ return void 0;
434
+ }
435
+ const sanitized = sanitizeAddMenuHtmlFragment(htmlValue);
436
+ if (byteLengthUtf8(sanitized) > HTML_HARD_LIMIT_BYTES) {
437
+ errors.push({
438
+ path: `${itemPath}.html`,
439
+ message: `html-fragment exceeds ${HTML_HARD_LIMIT_BYTES} bytes after sanitize`,
440
+ code: "html-fragment-oversize-hard"
441
+ });
442
+ return void 0;
443
+ }
444
+ return { type: "html-fragment", html: htmlValue };
445
+ }
446
+ const BROWSE_SIZES = /* @__PURE__ */ new Set(["large", "medium", "small"]);
447
+ function validateBrowse(raw, itemPath, errors) {
448
+ if (raw === void 0)
449
+ return void 0;
450
+ if (!isRecord(raw)) {
451
+ errors.push({
452
+ path: itemPath,
453
+ message: "browse must be an object",
454
+ code: "browse-not-object"
455
+ });
456
+ return void 0;
457
+ }
458
+ const sizeRaw = raw.size;
459
+ if (sizeRaw === void 0) {
460
+ return {};
461
+ }
462
+ if (typeof sizeRaw !== "string" || !BROWSE_SIZES.has(sizeRaw)) {
463
+ errors.push({
464
+ path: `${itemPath}.size`,
465
+ message: 'browse.size must be "large", "medium", or "small"',
466
+ code: "browse-unknown-size"
467
+ });
468
+ return void 0;
469
+ }
470
+ return { size: sizeRaw };
471
+ }
472
+ function validateFolderPath(raw, itemPath, errors) {
473
+ if (raw === void 0)
474
+ return void 0;
475
+ if (!Array.isArray(raw)) {
476
+ errors.push({
477
+ path: `${itemPath}.folderPath`,
478
+ message: "folderPath must be an array of strings",
479
+ code: "folder-path-not-array"
480
+ });
481
+ return void 0;
482
+ }
483
+ const segments = [];
484
+ for (let index = 0; index < raw.length; index++) {
485
+ const value = raw[index];
486
+ if (typeof value !== "string") {
487
+ errors.push({
488
+ path: `${itemPath}.folderPath[${index}]`,
489
+ message: "folderPath segments must be strings",
490
+ code: "folder-path-segment-type"
491
+ });
492
+ return void 0;
493
+ }
494
+ const trimmed = value.trim();
495
+ if (trimmed.length === 0) {
496
+ errors.push({
497
+ path: `${itemPath}.folderPath[${index}]`,
498
+ message: "folderPath segments must be non-empty",
499
+ code: "folder-path-empty-segment"
500
+ });
501
+ return void 0;
502
+ }
503
+ if (trimmed === ".." || trimmed.includes("/") || trimmed.includes("\\")) {
504
+ errors.push({
505
+ path: `${itemPath}.folderPath[${index}]`,
506
+ message: 'folderPath segments must not contain ".." or path separators',
507
+ code: "folder-path-invalid-segment"
508
+ });
509
+ return void 0;
510
+ }
511
+ if (segments.at(-1) === trimmed) {
512
+ errors.push({
513
+ path: `${itemPath}.folderPath[${index}]`,
514
+ message: "folderPath must not contain duplicate adjacent segments",
515
+ code: "folder-path-adjacent-duplicate"
516
+ });
517
+ return void 0;
518
+ }
519
+ segments.push(trimmed);
520
+ }
521
+ if (segments.length > FOLDER_PATH_MAX_SEGMENTS) {
522
+ errors.push({
523
+ path: `${itemPath}.folderPath`,
524
+ message: `folderPath exceeds ${FOLDER_PATH_MAX_SEGMENTS} segments`,
525
+ code: "folder-path-too-deep"
526
+ });
527
+ return void 0;
528
+ }
529
+ return segments.length > 0 ? segments : void 0;
530
+ }
531
+ function validateAddMenuItem(raw, itemPath) {
532
+ const errors = [];
533
+ if (!isRecord(raw)) {
534
+ return {
535
+ item: null,
536
+ errors: [
537
+ { path: itemPath, message: "item must be an object", code: "item-not-object" }
538
+ ]
539
+ };
540
+ }
541
+ for (const field of REJECTED_ITEM_FIELDS) {
542
+ if (field in raw) {
543
+ errors.push({
544
+ path: `${itemPath}.${field}`,
545
+ message: `field "${field}" is not allowed in Add Menu catalog items`,
546
+ code: `rejected-field-${field}`
547
+ });
548
+ }
549
+ }
550
+ const id = requiredString(raw, "id", itemPath, errors, "missing-id");
551
+ const title = requiredString(raw, "title", itemPath, errors, "missing-title");
552
+ const category = requiredString(raw, "category", itemPath, errors, "missing-category");
553
+ const prompt = requiredString(raw, "prompt", itemPath, errors, "missing-prompt");
554
+ if (errors.length > 0 || !id || !title || !category || !prompt) {
555
+ return { item: null, errors };
556
+ }
557
+ const interaction = validateInteraction(raw.interaction, `${itemPath}.interaction`, errors);
558
+ const presentation = validatePresentation(raw.presentation, `${itemPath}.presentation`, errors);
559
+ const browse = validateBrowse(raw.browse, `${itemPath}.browse`, errors);
560
+ const folderPath = validateFolderPath(raw.folderPath, itemPath, errors);
561
+ if (errors.length > 0) {
562
+ return { item: null, errors };
563
+ }
564
+ return {
565
+ item: {
566
+ id,
567
+ title,
568
+ category,
569
+ prompt,
570
+ pluginName: optionalString(raw, "pluginName"),
571
+ packageName: optionalString(raw, "packageName"),
572
+ subCategory: optionalString(raw, "subCategory"),
573
+ ...folderPath ? { folderPath } : {},
574
+ thumbnail: optionalString(raw, "thumbnail"),
575
+ ...presentation ? { presentation } : {},
576
+ ...browse ? { browse } : {},
577
+ ...interaction ? { interaction } : {}
578
+ },
579
+ errors
580
+ };
581
+ }
582
+ function validateAddMenuCatalogFile(raw, sourcePath) {
583
+ const errors = [];
584
+ if (!isRecord(raw)) {
585
+ return {
586
+ file: null,
587
+ errors: [
588
+ {
589
+ path: sourcePath,
590
+ message: "catalog file must be an object",
591
+ code: "catalog-not-object"
592
+ }
593
+ ]
594
+ };
595
+ }
596
+ if (!Array.isArray(raw.items)) {
597
+ return {
598
+ file: null,
599
+ errors: [
600
+ {
601
+ path: `${sourcePath}.items`,
602
+ message: "items must be an array",
603
+ code: "catalog-items-not-array"
604
+ }
605
+ ]
606
+ };
607
+ }
608
+ const items = [];
609
+ raw.items.forEach((entry, index) => {
610
+ const result = validateAddMenuItem(entry, `${sourcePath}.items[${index}]`);
611
+ errors.push(...result.errors);
612
+ if (result.item)
613
+ items.push(result.item);
614
+ });
615
+ if (items.length === 0 && errors.length > 0) {
616
+ return { file: null, errors };
617
+ }
618
+ return { file: { items }, errors };
619
+ }
620
+ function normalizeAddMenuPresentation(item) {
621
+ if (item.presentation)
622
+ return item.presentation;
623
+ const thumbnail = item.thumbnail?.trim();
624
+ if (!thumbnail)
625
+ return void 0;
626
+ if (/\.gif$/i.test(thumbnail)) {
627
+ return { type: "gif", src: thumbnail };
628
+ }
629
+ return { type: "image", src: thumbnail };
630
+ }
631
+ function normalizeAddMenuBrowseSize(item) {
632
+ return item.browse?.size ?? "medium";
633
+ }
634
+ function hasSingleRootDiv(html) {
635
+ const trimmed = html.trim();
636
+ if (!trimmed.startsWith("<div"))
637
+ return false;
638
+ const openMatch = trimmed.match(/^<div\b[^>]*>/i);
639
+ if (!openMatch)
640
+ return false;
641
+ const afterOpen = trimmed.slice(openMatch[0].length);
642
+ const closeIdx = afterOpen.lastIndexOf("</div>");
643
+ if (closeIdx < 0)
644
+ return false;
645
+ const tail = afterOpen.slice(closeIdx + "</div>".length).trim();
646
+ return tail.length === 0;
647
+ }
648
+ function hasAtScopeStyle(html) {
649
+ return /<style\b[^>]*>[\s\S]*@scope\b/i.test(html);
650
+ }
651
+ function lintHtmlFragment(item, sourcePath) {
652
+ const errors = [];
653
+ const warnings = [];
654
+ const presentation = item.presentation;
655
+ if (!presentation || presentation.type !== "html-fragment") {
656
+ return { errors, warnings };
657
+ }
658
+ const rawHtml = presentation.html?.trim() ?? "";
659
+ if (!rawHtml) {
660
+ errors.push(
661
+ catalogWarning(
662
+ "html-fragment-missing-html",
663
+ "html-fragment requires non-empty presentation.html",
664
+ item.id,
665
+ sourcePath
666
+ )
667
+ );
668
+ return { errors, warnings };
669
+ }
670
+ const unsafe = detectUnsafeAddMenuHtmlFragment(rawHtml);
671
+ if (unsafe) {
672
+ errors.push(catalogWarning(unsafe.code, unsafe.message, item.id, sourcePath));
673
+ return { errors, warnings };
674
+ }
675
+ const sanitized = sanitizeAddMenuHtmlFragment(rawHtml);
676
+ const bytes = byteLengthUtf8(sanitized);
677
+ if (bytes > HTML_HARD_LIMIT_BYTES) {
678
+ errors.push(
679
+ catalogWarning(
680
+ "html-fragment-oversize-hard",
681
+ `html-fragment exceeds ${HTML_HARD_LIMIT_BYTES} bytes after sanitize (${bytes})`,
682
+ item.id,
683
+ sourcePath
684
+ )
685
+ );
686
+ } else if (bytes > HTML_SOFT_LIMIT_BYTES) {
687
+ warnings.push(
688
+ catalogWarning(
689
+ "html-fragment-oversize-soft",
690
+ `html-fragment exceeds ${HTML_SOFT_LIMIT_BYTES} bytes after sanitize (${bytes}) — consider trimming`,
691
+ item.id,
692
+ sourcePath
693
+ )
694
+ );
695
+ }
696
+ if (!hasSingleRootDiv(sanitized)) {
697
+ warnings.push(
698
+ catalogWarning(
699
+ "html-fragment-missing-root-div",
700
+ "html-fragment should use a single root <div>",
701
+ item.id,
702
+ sourcePath
703
+ )
704
+ );
705
+ }
706
+ if (!hasAtScopeStyle(sanitized)) {
707
+ warnings.push(
708
+ catalogWarning(
709
+ "html-fragment-missing-at-scope",
710
+ "html-fragment should include <style> with @scope for preview isolation",
711
+ item.id,
712
+ sourcePath
713
+ )
714
+ );
715
+ }
716
+ return { errors, warnings };
717
+ }
718
+ function lintGifPoster(item, sourcePath) {
719
+ const presentation = normalizeAddMenuPresentation(item);
720
+ if (presentation?.type !== "gif" || presentation.poster?.trim())
721
+ return [];
722
+ return [
723
+ catalogWarning(
724
+ "gif-missing-poster",
725
+ `gif item "${item.id}" has no poster — add poster for prefers-reduced-motion accessibility`,
726
+ item.id,
727
+ sourcePath
728
+ )
729
+ ];
730
+ }
731
+ function lintBrowseLargeWithoutPresentation(item, sourcePath) {
732
+ if (normalizeAddMenuBrowseSize(item) !== "large")
733
+ return [];
734
+ if (normalizeAddMenuPresentation(item))
735
+ return [];
736
+ return [
737
+ catalogWarning(
738
+ "browse-large-without-presentation",
739
+ `large browse item "${item.id}" has no presentation or thumbnail — add a preview`,
740
+ item.id,
741
+ sourcePath
742
+ )
743
+ ];
744
+ }
745
+ function lintAddMenuCatalog(items, sourcePath) {
746
+ const errors = [];
747
+ const warnings = [];
748
+ for (const item of items) {
749
+ warnings.push(...lintGifPoster(item, sourcePath));
750
+ warnings.push(...lintBrowseLargeWithoutPresentation(item, sourcePath));
751
+ const htmlLint = lintHtmlFragment(item, sourcePath);
752
+ errors.push(...htmlLint.errors);
753
+ warnings.push(...htmlLint.warnings);
754
+ }
755
+ return { errors, warnings };
756
+ }
757
+ const ADD_MENU_VALIDATION_SUGGESTIONS = {
758
+ "gif-missing-poster": "Add presentation.poster with a static image for prefers-reduced-motion accessibility",
759
+ "html-fragment-missing-root-div": 'Wrap preview markup in a single root <div class="am-preview"> — see agent-kit/plugin/aiditor-add-menu.md',
760
+ "html-fragment-missing-at-scope": "Add <style> with @scope (.am-preview) inside the root div — see agent-kit/plugin/aiditor-add-menu.md",
761
+ "html-fragment-missing-html": "Set presentation.html with inline markup — external preview files are not supported",
762
+ "html-fragment-src-not-allowed": "Remove presentation.src; use presentation.html for html-fragment previews",
763
+ "html-fragment-unsafe-markup": "Remove scripts, event handlers, and javascript: URLs from presentation.html",
764
+ "html-fragment-oversize-soft": "Trim inline html or use a gif preview for rich motion",
765
+ "html-fragment-oversize-hard": "Reduce presentation.html below 32 KB after sanitize",
766
+ "browse-large-without-presentation": "Add presentation or thumbnail — large browse tiles need a visual preview",
767
+ "browse-unknown-size": "Set browse.size to large, medium, or small",
768
+ "presentation-unknown-type": "Set presentation.type to image, gif, or html-fragment",
769
+ "interaction-invalid-mode": "Set interaction.mode to reference (default) or stage-place",
770
+ "folder-path-not-array": "folderPath must be a yaml array of folder name strings",
771
+ "folder-path-segment-type": "Each folderPath entry must be a non-empty string",
772
+ "folder-path-empty-segment": "Remove empty folderPath segments",
773
+ "folder-path-invalid-segment": 'Use separate array elements per folder level — no "/" or "\\" in segment names',
774
+ "folder-path-adjacent-duplicate": "Remove duplicate consecutive folderPath segments",
775
+ "folder-path-too-deep": `Shorten folderPath to at most ${FOLDER_PATH_MAX_SEGMENTS} segments`,
776
+ "rejected-field-kind": "Remove kind — express item intent in prompt text",
777
+ "rejected-field-parameters": "Remove parameters — materialize values into prompt",
778
+ "rejected-field-component": "Remove component — use prompt plus contract paths in agent-kit",
779
+ "rejected-field-allowedScopes": "Remove allowedScopes — attachment scope is chosen in the AIditor UI",
780
+ "add-menu-missing-agentkit-handler": "Declare agentkit in plugin.yaml pointing at an agent-kit handler export; run jay-stack agent-kit (project yarn agent-kit) to materialize catalogs — not jay-stack setup",
781
+ "add-menu-legacy-setup-handler": "Move add-menu catalog writes from the setup handler to agentkit. Setup is for config/credentials only; run jay-stack agent-kit to generate agent-kit/aiditor/add-menu/*.yaml",
782
+ "deprecated-nested-setup-keys": "Replace nested setup.handler / setup.references with flat keys: setup: <handler> and agentkit: <handler>"
783
+ };
784
+ const ADD_MENU_CATALOG_REL_PATHS = [
785
+ "agent-kit/aiditor/add-menu.template.yaml",
786
+ "agent-kit/aiditor/add-menu.yaml"
787
+ ];
788
+ const CONTRIBUTOR_GUIDE = "agent-kit/plugin/aiditor-add-menu.md";
789
+ const ADD_MENU_WRITE_MARKERS = [
790
+ "agent-kit/aiditor/add-menu",
791
+ "aiditor-add-menu-thumbnails",
792
+ "writeAddMenuCatalog",
793
+ "copyAiditorAddMenuThumbnails"
794
+ ];
795
+ function isRelativeHandlerRef(value) {
796
+ return value.startsWith("./") || value.startsWith("../");
797
+ }
798
+ function resolveModulePath$1(basePath) {
799
+ for (const ext of ["", ".ts", ".js", "/index.ts", "/index.js"]) {
800
+ const candidate = basePath + ext;
801
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
802
+ return candidate;
803
+ }
804
+ }
805
+ return void 0;
806
+ }
807
+ function collectTypeScriptFiles(dir, depth = 0) {
808
+ if (depth > 4)
809
+ return [];
810
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
811
+ const files = [];
812
+ for (const entry of entries) {
813
+ const fullPath = path.join(dir, entry.name);
814
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "test") {
815
+ files.push(...collectTypeScriptFiles(fullPath, depth + 1));
816
+ } else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
817
+ files.push(fullPath);
818
+ }
819
+ }
820
+ return files;
821
+ }
822
+ function resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage) {
823
+ if (isRelativeHandlerRef(handlerRef)) {
824
+ return resolveModulePath$1(path.join(pluginPath, handlerRef)) ?? null;
825
+ }
826
+ const searchRoots = isNpmPackage ? [path.join(pluginPath, "lib"), path.join(pluginPath, "dist")] : [pluginPath];
827
+ for (const root of searchRoots) {
828
+ if (!fs.existsSync(root))
829
+ continue;
830
+ for (const file of collectTypeScriptFiles(root)) {
831
+ const content = fs.readFileSync(file, "utf-8");
832
+ const definesHandler = new RegExp(
833
+ `export\\s+(?:async\\s+)?function\\s+${handlerRef}\\b`
834
+ ).test(content);
835
+ const definesDefaultNamed = new RegExp(
836
+ `export\\s+default\\s+async\\s+function\\s+${handlerRef}\\b`
837
+ ).test(content);
838
+ if (definesHandler || definesDefaultNamed) {
839
+ return file;
840
+ }
841
+ const reExportMatch = content.match(
842
+ new RegExp(
843
+ `export\\s*\\{[^}]*\\b${handlerRef}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`
844
+ )
845
+ );
846
+ if (reExportMatch) {
847
+ const importSpec = reExportMatch[1].replace(/\.js$/, "");
848
+ const resolved = resolveModulePath$1(path.resolve(path.dirname(file), importSpec));
849
+ if (resolved)
850
+ return resolved;
851
+ }
852
+ }
853
+ }
854
+ return null;
855
+ }
856
+ function extractBalancedBlock(source, openBraceIndex) {
857
+ let depth = 0;
858
+ for (let index = openBraceIndex; index < source.length; index++) {
859
+ const char = source[index];
860
+ if (char === "{")
861
+ depth++;
862
+ else if (char === "}") {
863
+ depth--;
864
+ if (depth === 0) {
865
+ return source.slice(openBraceIndex, index + 1);
866
+ }
867
+ }
868
+ }
869
+ return null;
870
+ }
871
+ function findFunctionBodyOpenBrace(source, searchFrom) {
872
+ const arrowMatch = /=>\s*\{/.exec(source.slice(searchFrom));
873
+ const parenMatch = /\)\s*(?::[^{]+)?\{/.exec(source.slice(searchFrom));
874
+ const candidates = [];
875
+ if (arrowMatch?.index !== void 0) {
876
+ candidates.push(searchFrom + arrowMatch.index + arrowMatch[0].length - 1);
877
+ }
878
+ if (parenMatch?.index !== void 0) {
879
+ candidates.push(searchFrom + parenMatch.index + parenMatch[0].length - 1);
880
+ }
881
+ if (candidates.length === 0)
882
+ return -1;
883
+ return Math.min(...candidates);
884
+ }
885
+ function extractFunctionBody(source, functionName) {
886
+ const patterns = [
887
+ new RegExp(`export\\s+async\\s+function\\s+${functionName}\\b`),
888
+ new RegExp(`export\\s+function\\s+${functionName}\\b`),
889
+ new RegExp(`export\\s+default\\s+async\\s+function\\s+${functionName}\\b`),
890
+ new RegExp(
891
+ `export\\s+const\\s+${functionName}\\s*=\\s*async\\s*\\([^)]*\\)\\s*(?::[^{]+)?=>\\s*\\{`
892
+ ),
893
+ new RegExp(
894
+ `export\\s+const\\s+${functionName}\\s*=\\s*\\([^)]*\\)\\s*(?::[^{]+)?=>\\s*\\{`
895
+ )
896
+ ];
897
+ for (const pattern of patterns) {
898
+ const match = pattern.exec(source);
899
+ if (!match)
900
+ continue;
901
+ const braceIndex = findFunctionBodyOpenBrace(source, match.index);
902
+ if (braceIndex === -1)
903
+ continue;
904
+ return extractBalancedBlock(source, braceIndex);
905
+ }
906
+ return null;
907
+ }
908
+ function extractDefaultExportFunctionBody(source) {
909
+ const patterns = [
910
+ /export\s+default\s+async\s+function\s+\w+\b/,
911
+ /export\s+default\s+async\s+function\s*\(/,
912
+ /export\s+default\s+async\s*\([^)]*\)\s*(?::[^{]+)?=>\s*\{/
913
+ ];
914
+ for (const pattern of patterns) {
915
+ const match = pattern.exec(source);
916
+ if (!match)
917
+ continue;
918
+ const braceIndex = findFunctionBodyOpenBrace(source, match.index);
919
+ if (braceIndex === -1)
920
+ continue;
921
+ return extractBalancedBlock(source, braceIndex);
922
+ }
923
+ return null;
924
+ }
925
+ function handlerBodyWritesAddMenuCatalog(body) {
926
+ return ADD_MENU_WRITE_MARKERS.some((marker) => body.includes(marker));
927
+ }
928
+ function resolveSetupHandlerFunctionBody(pluginPath, handlerRef, isNpmPackage) {
929
+ const sourceFile = resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage);
930
+ if (!sourceFile)
931
+ return null;
932
+ const source = fs.readFileSync(sourceFile, "utf-8");
933
+ if (isRelativeHandlerRef(handlerRef)) {
934
+ return extractDefaultExportFunctionBody(source) ?? extractFunctionBody(source, "setup") ?? extractFunctionBody(source, handlerRef);
935
+ }
936
+ return extractFunctionBody(source, handlerRef);
937
+ }
938
+ function suggestionForCode(code) {
939
+ if (!code)
940
+ return `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
941
+ return ADD_MENU_VALIDATION_SUGGESTIONS[code] ?? `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
942
+ }
943
+ function mapSchemaError(error, catalogPath) {
944
+ const code = error.code ?? "catalog-validation-error";
945
+ return {
946
+ type: "add-menu-catalog",
947
+ code,
948
+ message: `[${code}] ${error.message}`,
949
+ location: error.path || catalogPath,
950
+ suggestion: suggestionForCode(code)
951
+ };
952
+ }
953
+ function mapLintFinding(finding, catalogPath, severity) {
954
+ return {
955
+ type: "add-menu-catalog",
956
+ code: finding.code,
957
+ message: `[${finding.code}] ${finding.message}`,
958
+ location: finding.sourcePath ?? catalogPath,
959
+ itemId: finding.itemId,
960
+ suggestion: suggestionForCode(finding.code),
961
+ ...severity === "warning" ? {} : {}
962
+ };
963
+ }
964
+ function pluginShipsAddMenuCatalog(context) {
965
+ return ADD_MENU_CATALOG_REL_PATHS.some(
966
+ (relPath) => fs.existsSync(path.join(context.pluginPath, relPath))
967
+ );
968
+ }
969
+ function validateAddMenuAgentKitHandler(context, result) {
970
+ if (!pluginShipsAddMenuCatalog(context))
971
+ return;
972
+ const agentKitHandler = context.manifest.agentkit;
973
+ if (!agentKitHandler) {
974
+ result.warnings.push({
975
+ type: "add-menu-catalog",
976
+ code: "add-menu-missing-agentkit-handler",
977
+ message: "[add-menu-missing-agentkit-handler] Plugin ships add-menu catalog yaml but plugin.yaml has no agentkit handler — catalogs must be generated during jay-stack agent-kit (yarn agent-kit), not jay-stack setup",
978
+ location: "plugin.yaml",
979
+ suggestion: suggestionForCode("add-menu-missing-agentkit-handler")
980
+ });
981
+ }
982
+ const setupHandler = typeof context.manifest.setup === "string" ? context.manifest.setup : void 0;
983
+ if (!setupHandler)
984
+ return;
985
+ const setupBody = resolveSetupHandlerFunctionBody(
986
+ context.pluginPath,
987
+ setupHandler,
988
+ context.isNpmPackage
989
+ );
990
+ if (!setupBody || !handlerBodyWritesAddMenuCatalog(setupBody))
991
+ return;
992
+ result.warnings.push({
993
+ type: "add-menu-catalog",
994
+ code: "add-menu-legacy-setup-handler",
995
+ message: "[add-menu-legacy-setup-handler] setup handler writes add-menu catalogs — move catalog materialization to agentkit and run jay-stack agent-kit (yarn agent-kit); keep setup for config and credentials only",
996
+ location: `plugin.yaml setup (${setupHandler})`,
997
+ suggestion: suggestionForCode("add-menu-legacy-setup-handler")
998
+ });
999
+ }
1000
+ async function validateAddMenuCatalogFileAtPath(catalogPath, relPath, result) {
1001
+ let parsed;
1002
+ try {
1003
+ const content = await fs.promises.readFile(catalogPath, "utf-8");
1004
+ parsed = YAML.parse(content);
1005
+ } catch (error) {
1006
+ const message = error instanceof Error ? error.message : String(error);
1007
+ result.errors.push({
1008
+ type: "add-menu-catalog",
1009
+ code: "catalog-yaml-parse-error",
1010
+ message: `Invalid add-menu catalog YAML: ${message}`,
1011
+ location: relPath,
1012
+ suggestion: `Check YAML syntax in the add-menu catalog file. See ${CONTRIBUTOR_GUIDE}`
1013
+ });
1014
+ return;
1015
+ }
1016
+ const validated = validateAddMenuCatalogFile(parsed, relPath);
1017
+ result.errors.push(...validated.errors.map((error) => mapSchemaError(error, relPath)));
1018
+ if (!validated.file?.items.length) {
1019
+ return;
1020
+ }
1021
+ const linted = lintAddMenuCatalog(validated.file.items, relPath);
1022
+ result.errors.push(
1023
+ ...linted.errors.map((finding) => mapLintFinding(finding, relPath, "error"))
1024
+ );
1025
+ result.warnings.push(
1026
+ ...linted.warnings.map((finding) => mapLintFinding(finding, relPath, "warning"))
1027
+ );
1028
+ }
1029
+ async function validateAddMenuCatalog(context, result) {
1030
+ validateAddMenuAgentKitHandler(context, result);
1031
+ for (const relPath of ADD_MENU_CATALOG_REL_PATHS) {
1032
+ const catalogPath = path.join(context.pluginPath, relPath);
1033
+ if (!fs.existsSync(catalogPath))
1034
+ continue;
1035
+ await validateAddMenuCatalogFileAtPath(catalogPath, relPath, result);
1036
+ }
1037
+ }
275
1038
  async function validatePlugin(options = {}) {
276
1039
  const pluginPath = options.pluginPath || process.cwd();
277
1040
  if (options.local) {
@@ -338,6 +1101,7 @@ async function validatePluginPackage(pluginPath, options) {
338
1101
  if (pluginManifest.dynamic_contracts) {
339
1102
  await validateDynamicContracts(context, result);
340
1103
  }
1104
+ await validateAddMenuCatalog(context, result);
341
1105
  result.valid = result.errors.length === 0;
342
1106
  return result;
343
1107
  }
@@ -451,6 +1215,14 @@ async function validateSchema(context, result) {
451
1215
  location: "plugin.yaml",
452
1216
  suggestion: 'Specify the exported member name from the module (e.g., "moodTracker")'
453
1217
  });
1218
+ } else {
1219
+ validateHandlerRef(
1220
+ contract.component,
1221
+ `Contract "${contract.name}" component`,
1222
+ `plugin.yaml contracts[${index}]`,
1223
+ context,
1224
+ result
1225
+ );
454
1226
  }
455
1227
  });
456
1228
  }
@@ -483,6 +1255,24 @@ async function validateSchema(context, result) {
483
1255
  suggestion: 'Specify prefix for dynamic contract names (e.g., "cms")'
484
1256
  });
485
1257
  }
1258
+ if (config.component) {
1259
+ validateHandlerRef(
1260
+ config.component,
1261
+ `dynamic_contracts[${prefix}] component`,
1262
+ `plugin.yaml dynamic_contracts`,
1263
+ context,
1264
+ result
1265
+ );
1266
+ }
1267
+ if (config.generator) {
1268
+ validateHandlerRef(
1269
+ config.generator,
1270
+ `dynamic_contracts[${prefix}] generator`,
1271
+ `plugin.yaml dynamic_contracts`,
1272
+ context,
1273
+ result
1274
+ );
1275
+ }
486
1276
  }
487
1277
  }
488
1278
  if (!manifest.contracts && !manifest.dynamic_contracts) {
@@ -553,6 +1343,23 @@ async function validateSchema(context, result) {
553
1343
  });
554
1344
  }
555
1345
  }
1346
+ if (manifest.actions) {
1347
+ for (const entry of manifest.actions) {
1348
+ const exportName = typeof entry === "string" ? entry : entry.name;
1349
+ if (exportName) {
1350
+ validateHandlerRef(
1351
+ exportName,
1352
+ `Action "${exportName}"`,
1353
+ "plugin.yaml actions",
1354
+ context,
1355
+ result
1356
+ );
1357
+ }
1358
+ }
1359
+ }
1360
+ if (manifest.init) {
1361
+ validateHandlerRef(manifest.init, "Init handler", "plugin.yaml init", context, result);
1362
+ }
556
1363
  if (manifest.routes) {
557
1364
  if (!Array.isArray(manifest.routes)) {
558
1365
  result.errors.push({
@@ -584,6 +1391,14 @@ async function validateSchema(context, result) {
584
1391
  location: "plugin.yaml",
585
1392
  suggestion: "Specify the exported member name for the page component"
586
1393
  });
1394
+ } else {
1395
+ validateHandlerRef(
1396
+ route.component,
1397
+ `Route "${route.path}" component`,
1398
+ `plugin.yaml routes`,
1399
+ context,
1400
+ result
1401
+ );
587
1402
  }
588
1403
  if (route.jayHtml) {
589
1404
  validateDocFile(
@@ -623,22 +1438,112 @@ async function validateSchema(context, result) {
623
1438
  suggestion: "Specify the relative path to the validator handler module"
624
1439
  });
625
1440
  }
626
- if (validator.handler && !context.isNpmPackage) {
627
- const handlerPath = path.join(context.pluginPath, validator.handler);
628
- const extensions = ["", ".ts", ".js", "/index.ts", "/index.js"];
629
- const found = extensions.some((ext) => fs.existsSync(handlerPath + ext));
630
- if (!found) {
631
- result.errors.push({
632
- type: "file-missing",
633
- message: `Validator "${validator.name}" handler not found: ${validator.handler}`,
634
- location: "plugin.yaml validators",
635
- suggestion: `Create the validator handler at ${handlerPath}.ts`
636
- });
637
- }
1441
+ if (validator.handler) {
1442
+ validateHandlerRef(
1443
+ validator.handler,
1444
+ `Validator "${validator.name}" handler`,
1445
+ "plugin.yaml validators",
1446
+ context,
1447
+ result
1448
+ );
638
1449
  }
639
1450
  });
640
1451
  }
641
1452
  }
1453
+ if (manifest.setup) {
1454
+ if (typeof manifest.setup !== "string") {
1455
+ result.errors.push({
1456
+ type: "schema",
1457
+ message: "Deprecated nested setup keys (setup.handler / setup.references) — use flat setup: and agentkit: in plugin.yaml",
1458
+ location: "plugin.yaml setup",
1459
+ suggestion: "Replace setup.handler with top-level setup: and setup.references with top-level agentkit:"
1460
+ });
1461
+ } else {
1462
+ validateHandlerRef(
1463
+ manifest.setup,
1464
+ "Setup handler",
1465
+ "plugin.yaml setup",
1466
+ context,
1467
+ result
1468
+ );
1469
+ }
1470
+ }
1471
+ if (manifest.agentkit) {
1472
+ validateHandlerRef(
1473
+ manifest.agentkit,
1474
+ "Agent-kit handler",
1475
+ "plugin.yaml agentkit",
1476
+ context,
1477
+ result
1478
+ );
1479
+ }
1480
+ }
1481
+ function checkExportExists(exportName, context) {
1482
+ const packageJsonPath = path.join(context.pluginPath, "package.json");
1483
+ if (!fs.existsSync(packageJsonPath))
1484
+ return true;
1485
+ let mainPath;
1486
+ try {
1487
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
1488
+ if (packageJson.exports?.["."]) {
1489
+ const entry = packageJson.exports["."];
1490
+ const entryPath = typeof entry === "string" ? entry : entry.default || entry.import;
1491
+ if (entryPath)
1492
+ mainPath = path.join(context.pluginPath, entryPath);
1493
+ }
1494
+ if (!mainPath && packageJson.main) {
1495
+ mainPath = path.join(context.pluginPath, packageJson.main);
1496
+ }
1497
+ } catch {
1498
+ return true;
1499
+ }
1500
+ if (!mainPath || !fs.existsSync(mainPath))
1501
+ return true;
1502
+ try {
1503
+ const content = fs.readFileSync(mainPath, "utf-8");
1504
+ const patterns = [
1505
+ new RegExp(`export\\s*\\{[^}]*\\b${exportName}\\b[^}]*\\}`, "m"),
1506
+ new RegExp(`export\\s+(?:async\\s+)?function\\s+${exportName}\\b`),
1507
+ new RegExp(`export\\s+(?:const|let|var)\\s+${exportName}\\b`)
1508
+ ];
1509
+ return patterns.some((p) => p.test(content));
1510
+ } catch {
1511
+ return true;
1512
+ }
1513
+ }
1514
+ function isRelativePath(value) {
1515
+ return value.startsWith("./") || value.startsWith("../");
1516
+ }
1517
+ function validateHandlerRef(value, label, location, context, result) {
1518
+ if (context.isNpmPackage) {
1519
+ if (isRelativePath(value)) {
1520
+ result.errors.push({
1521
+ type: "export-mismatch",
1522
+ message: `${label} "${value}" is a relative path, but NPM plugins must use an export name`,
1523
+ location,
1524
+ suggestion: `Export the function from the package entry point and use the export name instead of a path`
1525
+ });
1526
+ } else if (!checkExportExists(value, context)) {
1527
+ result.errors.push({
1528
+ type: "export-mismatch",
1529
+ message: `${label} "${value}" is not exported from the package`,
1530
+ location,
1531
+ suggestion: `Add "export { ${value} } from '...'" to the package entry point`
1532
+ });
1533
+ }
1534
+ } else if (isRelativePath(value)) {
1535
+ const handlerPath = path.join(context.pluginPath, value);
1536
+ const extensions = ["", ".ts", ".js", "/index.ts", "/index.js"];
1537
+ const found = extensions.some((ext) => fs.existsSync(handlerPath + ext));
1538
+ if (!found) {
1539
+ result.errors.push({
1540
+ type: "file-missing",
1541
+ message: `${label} not found: ${value}`,
1542
+ location,
1543
+ suggestion: `Create the handler at ${handlerPath}.ts`
1544
+ });
1545
+ }
1546
+ }
642
1547
  }
643
1548
  function resolveContractFile(contractSpec, context) {
644
1549
  if (context.isNpmPackage) {
@@ -958,6 +1863,18 @@ async function validatePackageJson(context, result) {
958
1863
  suggestion: 'Add "./plugin.yaml": "./plugin.yaml" to exports field'
959
1864
  });
960
1865
  }
1866
+ const agentKitDir = path.join(context.pluginPath, "agent-kit");
1867
+ if (fs.existsSync(agentKitDir) && fs.statSync(agentKitDir).isDirectory()) {
1868
+ const filesArray = packageJson.files;
1869
+ if (!filesArray || !filesArray.includes("agent-kit")) {
1870
+ result.warnings.push({
1871
+ type: "export-mismatch",
1872
+ message: 'agent-kit directory exists but is not listed in package.json "files"',
1873
+ location: packageJsonPath,
1874
+ suggestion: 'Add "agent-kit" to the "files" array so agent-kit files are shipped with the package'
1875
+ });
1876
+ }
1877
+ }
961
1878
  } catch (error) {
962
1879
  result.errors.push({
963
1880
  type: "schema",
@@ -1020,6 +1937,10 @@ async function validateDynamicContracts(context, result) {
1020
1937
  }
1021
1938
  }
1022
1939
  export {
1940
+ ADD_MENU_VALIDATION_SUGGESTIONS,
1023
1941
  checkComponentPropsAndParams,
1942
+ lintAddMenuCatalog,
1943
+ validateAddMenuCatalogFile,
1944
+ validateAddMenuItem,
1024
1945
  validatePlugin
1025
1946
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/plugin-validator",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Validation tool for Jay Stack plugins",
@@ -21,19 +21,19 @@
21
21
  "build:check-types": "tsc",
22
22
  "clean": "rimraf dist",
23
23
  "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
24
- "test": ":",
25
- "test:watch": ":"
24
+ "test": "vitest run",
25
+ "test:watch": "vitest"
26
26
  },
27
27
  "dependencies": {
28
- "@jay-framework/compiler-jay-html": "^0.20.0",
29
- "@jay-framework/compiler-shared": "^0.20.0",
30
- "@jay-framework/editor-protocol": "^0.20.0",
31
- "@jay-framework/typescript-bridge": "^0.20.0",
28
+ "@jay-framework/compiler-jay-html": "^0.22.0",
29
+ "@jay-framework/compiler-shared": "^0.22.0",
30
+ "@jay-framework/editor-protocol": "^0.22.0",
31
+ "@jay-framework/typescript-bridge": "^0.22.0",
32
32
  "chalk": "^4.1.2",
33
33
  "yaml": "^2.3.4"
34
34
  },
35
35
  "devDependencies": {
36
- "@jay-framework/dev-environment": "^0.20.0",
36
+ "@jay-framework/dev-environment": "^0.22.0",
37
37
  "@types/node": "^22.15.21",
38
38
  "rimraf": "^5.0.5",
39
39
  "tsup": "^8.0.1",