jui-tools-mcp-server 2.13.0 → 2.14.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.
@@ -2,6 +2,7 @@
2
2
  "_description": "Per-component presentation metadata consumed by jsonui-mcp-server. Augments attribute_definitions.json (which defines the raw attribute schema, including binding_direction per attribute) with human descriptions, aliases, platform availability, platform-specific implementation notes, and agent-facing rules. Edit this file to change what `lookup_component` / `search_components` / `lookup_attribute` return from the MCP.",
3
3
  "_schema": {
4
4
  "description": "string",
5
+ "interactive": "boolean — REQUIRED on every type: whether a user can operate this component on its own (a control, an input, or a scrolling / embedding container). A tappable that holds one is not flattened into a single button for screen readers (shared/core/tap_accessibility.rb). Declared, never derived: Button, Radio and Web declare no two-way binding and are operable.",
5
6
  "aliases": "string[]",
6
7
  "platforms": {
7
8
  "swift_generated": "boolean",
@@ -15,6 +16,7 @@
15
16
  },
16
17
  "Label": {
17
18
  "description": "Text display component",
19
+ "interactive": false,
18
20
  "aliases": [
19
21
  "Text"
20
22
  ],
@@ -49,6 +51,7 @@
49
51
  },
50
52
  "TextField": {
51
53
  "description": "Text input field",
54
+ "interactive": true,
52
55
  "aliases": [
53
56
  "EditText",
54
57
  "Input"
@@ -86,6 +89,7 @@
86
89
  },
87
90
  "Button": {
88
91
  "description": "Tappable button component",
92
+ "interactive": true,
89
93
  "aliases": [],
90
94
  "platforms": {
91
95
  "swift_generated": true,
@@ -117,6 +121,7 @@
117
121
  },
118
122
  "Image": {
119
123
  "description": "Local image component",
124
+ "interactive": false,
120
125
  "aliases": [
121
126
  "CircleImage",
122
127
  "CircleImageView",
@@ -153,6 +158,7 @@
153
158
  },
154
159
  "NetworkImage": {
155
160
  "description": "Remote/network image component",
161
+ "interactive": false,
156
162
  "aliases": [],
157
163
  "platforms": {
158
164
  "swift_generated": true,
@@ -174,12 +180,12 @@
174
180
  }
175
181
  },
176
182
  "rules": [
177
- "Default contentMode is .center for matchParent (Swift)",
178
183
  "Supports placeholder/error images via defaultImage"
179
184
  ]
180
185
  },
181
186
  "View": {
182
187
  "description": "Container view (HStack/VStack/ZStack)",
188
+ "interactive": false,
183
189
  "aliases": [],
184
190
  "platforms": {
185
191
  "swift_generated": true,
@@ -212,6 +218,7 @@
212
218
  },
213
219
  "ScrollView": {
214
220
  "description": "Scrollable container",
221
+ "interactive": true,
215
222
  "aliases": [],
216
223
  "platforms": {
217
224
  "swift_generated": true,
@@ -238,6 +245,7 @@
238
245
  },
239
246
  "Collection": {
240
247
  "description": "List/grid collection with data binding",
248
+ "interactive": true,
241
249
  "aliases": [
242
250
  "Table",
243
251
  "TableView",
@@ -270,11 +278,12 @@
270
278
  "Sections support different cell types per section",
271
279
  "cellIdProperty extracts unique keys from item data",
272
280
  "scrollTo binding enables programmatic scrolling",
273
- "lazy (default true) controls virtualization: true uses LazyVStack/LazyColumn/LazyVerticalGrid with internal scroll; false renders eagerly in a plain VStack/Column with NO scroll container (use when nested in an already-scrollable parent). Sticky headers and paging require lazy: true."
281
+ "lazy (default 'lazy') is the outer container: 'lazy' virtualizes (LazyVStack/LazyColumn/LazyVerticalGrid with internal scroll); 'eager' draws every cell in a scroll container, with no virtualization; 'none' draws them with NO scroll container (use when nested in an already-scrollable parent). Sticky headers and paging require 'lazy'."
274
282
  ]
275
283
  },
276
284
  "Switch": {
277
285
  "description": "Toggle switch (iOS style)",
286
+ "interactive": true,
278
287
  "aliases": [],
279
288
  "platforms": {
280
289
  "swift_generated": true,
@@ -302,6 +311,7 @@
302
311
  },
303
312
  "Toggle": {
304
313
  "description": "Toggle control (checkbox style)",
314
+ "interactive": true,
305
315
  "aliases": [],
306
316
  "platforms": {
307
317
  "swift_generated": true,
@@ -328,6 +338,7 @@
328
338
  },
329
339
  "CheckBox": {
330
340
  "description": "Checkbox input",
341
+ "interactive": true,
331
342
  "aliases": [
332
343
  "Check",
333
344
  "Checkbox"
@@ -357,6 +368,7 @@
357
368
  },
358
369
  "Radio": {
359
370
  "description": "Radio button group",
371
+ "interactive": true,
360
372
  "aliases": [],
361
373
  "platforms": {
362
374
  "swift_generated": true,
@@ -382,6 +394,7 @@
382
394
  },
383
395
  "SelectBox": {
384
396
  "description": "Dropdown/picker selection",
397
+ "interactive": true,
385
398
  "aliases": [],
386
399
  "platforms": {
387
400
  "swift_generated": true,
@@ -407,6 +420,7 @@
407
420
  },
408
421
  "Segment": {
409
422
  "description": "Segmented control",
423
+ "interactive": true,
410
424
  "aliases": [],
411
425
  "platforms": {
412
426
  "swift_generated": true,
@@ -433,6 +447,7 @@
433
447
  },
434
448
  "Slider": {
435
449
  "description": "Slider/range input",
450
+ "interactive": true,
436
451
  "aliases": [],
437
452
  "platforms": {
438
453
  "swift_generated": true,
@@ -454,12 +469,13 @@
454
469
  },
455
470
  "rules": [
456
471
  "value/bind is two-way binding",
457
- "minimumValue/maximumValue define range (default 0-100)",
472
+ "minimum / maximum define the range (default 0 and 1: with neither set the range is 0 .. 1); minimumValue / minValue and maximumValue / maxValue are alias spellings",
458
473
  "step defines discrete steps"
459
474
  ]
460
475
  },
461
476
  "Progress": {
462
477
  "description": "Progress bar/indicator",
478
+ "interactive": false,
463
479
  "aliases": [],
464
480
  "platforms": {
465
481
  "swift_generated": true,
@@ -485,6 +501,7 @@
485
501
  },
486
502
  "Indicator": {
487
503
  "description": "Activity indicator / loading spinner",
504
+ "interactive": false,
488
505
  "aliases": [],
489
506
  "platforms": {
490
507
  "swift_generated": true,
@@ -508,11 +525,12 @@
508
525
  },
509
526
  "CircleView": {
510
527
  "description": "Circular shape view",
528
+ "interactive": false,
511
529
  "aliases": [],
512
530
  "platforms": {
513
531
  "swift_generated": false,
514
532
  "swift_dynamic": false,
515
- "kotlin_generated": false,
533
+ "kotlin_generated": true,
516
534
  "kotlin_dynamic": true,
517
535
  "react": true
518
536
  },
@@ -531,6 +549,7 @@
531
549
  },
532
550
  "GradientView": {
533
551
  "description": "Gradient background view",
552
+ "interactive": false,
534
553
  "aliases": [],
535
554
  "platforms": {
536
555
  "swift_generated": true,
@@ -556,6 +575,7 @@
556
575
  },
557
576
  "Blur": {
558
577
  "description": "Blur effect view",
578
+ "interactive": false,
559
579
  "aliases": [],
560
580
  "platforms": {
561
581
  "swift_generated": true,
@@ -581,6 +601,7 @@
581
601
  },
582
602
  "IconLabel": {
583
603
  "description": "Icon with label combination",
604
+ "interactive": false,
584
605
  "aliases": [],
585
606
  "platforms": {
586
607
  "swift_generated": true,
@@ -606,6 +627,7 @@
606
627
  },
607
628
  "Web": {
608
629
  "description": "Web view / embedded browser",
630
+ "interactive": true,
609
631
  "aliases": [],
610
632
  "platforms": {
611
633
  "swift_generated": true,
@@ -626,11 +648,13 @@
626
648
  }
627
649
  },
628
650
  "rules": [
629
- "url attribute specifies the web page to load"
651
+ "url attribute specifies the web page to load",
652
+ "Error face + retry (iOS / Android only): bind onLoadFailed to a ViewModel method that hides the Web and shows the error, and bind reloadToken to an Int the retry method increments — the Web reloads its own url each time the value changes. Rewriting url to the same value does not reload."
630
653
  ]
631
654
  },
632
655
  "SafeAreaView": {
633
656
  "description": "Safe area respecting container",
657
+ "interactive": false,
634
658
  "aliases": [],
635
659
  "platforms": {
636
660
  "swift_generated": true,
@@ -653,6 +677,7 @@
653
677
  },
654
678
  "TabView": {
655
679
  "description": "Tab-based navigation container",
680
+ "interactive": true,
656
681
  "aliases": [],
657
682
  "platforms": {
658
683
  "swift_generated": true,
@@ -678,6 +703,7 @@
678
703
  },
679
704
  "TextView": {
680
705
  "description": "Multi-line text input",
706
+ "interactive": true,
681
707
  "aliases": [],
682
708
  "platforms": {
683
709
  "swift_generated": true,
@@ -704,11 +730,12 @@
704
730
  },
705
731
  "EditText": {
706
732
  "description": "Text input (Android naming convention)",
733
+ "interactive": true,
707
734
  "aliases": [],
708
735
  "platforms": {
709
736
  "swift_generated": true,
710
737
  "swift_dynamic": true,
711
- "kotlin_generated": false,
738
+ "kotlin_generated": true,
712
739
  "kotlin_dynamic": true,
713
740
  "react": true
714
741
  },
@@ -724,11 +751,12 @@
724
751
  },
725
752
  "Input": {
726
753
  "description": "Generic input (React naming convention)",
754
+ "interactive": true,
727
755
  "aliases": [],
728
756
  "platforms": {
729
757
  "swift_generated": true,
730
758
  "swift_dynamic": true,
731
- "kotlin_generated": false,
759
+ "kotlin_generated": true,
732
760
  "kotlin_dynamic": true,
733
761
  "react": true
734
762
  },
@@ -744,6 +772,7 @@
744
772
  },
745
773
  "Embed": {
746
774
  "description": "Embeds another screen as a region of this layout. The embedded screen owns its own ViewModel — independent from the parent VM.",
775
+ "interactive": true,
747
776
  "aliases": [],
748
777
  "platforms": {
749
778
  "swift_generated": true,
@@ -0,0 +1,167 @@
1
+ {
2
+ "_description": [
3
+ "Type-spelling synonyms: a node `type` that is not a section of attribute_definitions.json, mapped to the section that declares it (`canonical`).",
4
+ "The one table. Validation and the renderers are to read it, and hold no list of their own; which of them do is measured by their own tests (the type_synonyms_draw specs, SwiftJsonUI's TypeSynonymsDrawTests), not recorded here. It does not apply to sjui's UIKit mode, a separate renderer with its own set of UIKit components and its own spellings for them (Table is its SJUITableView); that mode draws what it has.",
5
+ "A renderer draws a synonym as its canonical type — except where the entry names `render_as`: the spelling draws differently from its canonical type (CircleImage clips to a circle), and the renderer draws `render_as` with the converter of that name, while validation still reads the canonical section. A type an app registers as its own component is the app's, whatever its spelling, and is never rewritten.",
6
+ "Any other key of an entry is an attribute the spelling itself means, of the canonical section: HStack and Row are a View with `orientation: horizontal`, VStack and Column one with `vertical`. A renderer adds it when the node does not set it. When the node sets it otherwise (an HStack with `orientation: vertical`), the node's value is drawn and the renderer warns, naming both. ZStack and Box add nothing: a View without orientation is already the stack they mean.",
7
+ "Not here: EditText / Input / Check / Toggle. They are sections of attribute_definitions.json that point at their canonical section with `_alias_of`; the exact-match lookup finds them first. Spacer / Divider / Triangle are neither declared nor synonyms, and draw as undeclared types."
8
+ ],
9
+ "synonyms": {
10
+ "Text": {
11
+ "canonical": "Label"
12
+ },
13
+ "MultiLineEditText": {
14
+ "canonical": "TextView"
15
+ },
16
+ "Textarea": {
17
+ "canonical": "TextView"
18
+ },
19
+ "ImageView": {
20
+ "canonical": "Image"
21
+ },
22
+ "Img": {
23
+ "canonical": "Image"
24
+ },
25
+ "NetworkImageView": {
26
+ "canonical": "NetworkImage"
27
+ },
28
+ "CircleImage": {
29
+ "canonical": "Image",
30
+ "render_as": "CircleImage"
31
+ },
32
+ "CircleImageView": {
33
+ "canonical": "Image",
34
+ "render_as": "CircleImage"
35
+ },
36
+ "AsyncImage": {
37
+ "canonical": "NetworkImage"
38
+ },
39
+ "Spinner": {
40
+ "canonical": "SelectBox"
41
+ },
42
+ "DatePicker": {
43
+ "canonical": "SelectBox"
44
+ },
45
+ "Select": {
46
+ "canonical": "SelectBox"
47
+ },
48
+ "Picker": {
49
+ "canonical": "SelectBox"
50
+ },
51
+ "Checkbox": {
52
+ "canonical": "CheckBox"
53
+ },
54
+ "RadioButton": {
55
+ "canonical": "Radio"
56
+ },
57
+ "RadioGroup": {
58
+ "canonical": "Radio"
59
+ },
60
+ "SegmentedControl": {
61
+ "canonical": "Segment"
62
+ },
63
+ "TabLayout": {
64
+ "canonical": "Segment"
65
+ },
66
+ "TabGroup": {
67
+ "canonical": "Segment"
68
+ },
69
+ "SeekBar": {
70
+ "canonical": "Slider"
71
+ },
72
+ "Range": {
73
+ "canonical": "Slider"
74
+ },
75
+ "ProgressBar": {
76
+ "canonical": "Progress"
77
+ },
78
+ "ActivityIndicator": {
79
+ "canonical": "Indicator"
80
+ },
81
+ "Loading": {
82
+ "canonical": "Indicator"
83
+ },
84
+ "LinearLayout": {
85
+ "canonical": "View"
86
+ },
87
+ "RelativeLayout": {
88
+ "canonical": "View"
89
+ },
90
+ "FrameLayout": {
91
+ "canonical": "View"
92
+ },
93
+ "HStack": {
94
+ "canonical": "View",
95
+ "orientation": "horizontal"
96
+ },
97
+ "VStack": {
98
+ "canonical": "View",
99
+ "orientation": "vertical"
100
+ },
101
+ "ZStack": {
102
+ "canonical": "View"
103
+ },
104
+ "Div": {
105
+ "canonical": "View"
106
+ },
107
+ "Box": {
108
+ "canonical": "View"
109
+ },
110
+ "Container": {
111
+ "canonical": "View"
112
+ },
113
+ "Column": {
114
+ "canonical": "View",
115
+ "orientation": "vertical"
116
+ },
117
+ "Row": {
118
+ "canonical": "View",
119
+ "orientation": "horizontal"
120
+ },
121
+ "ConstraintLayout": {
122
+ "canonical": "View"
123
+ },
124
+ "Scroll": {
125
+ "canonical": "ScrollView"
126
+ },
127
+ "CollectionView": {
128
+ "canonical": "Collection"
129
+ },
130
+ "RecyclerView": {
131
+ "canonical": "Collection"
132
+ },
133
+ "Table": {
134
+ "canonical": "Collection"
135
+ },
136
+ "TableView": {
137
+ "canonical": "Collection"
138
+ },
139
+ "List": {
140
+ "canonical": "Collection"
141
+ },
142
+ "Grid": {
143
+ "canonical": "Collection"
144
+ },
145
+ "LazyGrid": {
146
+ "canonical": "Collection"
147
+ },
148
+ "ListView": {
149
+ "canonical": "Collection"
150
+ },
151
+ "LazyColumn": {
152
+ "canonical": "Collection"
153
+ },
154
+ "Gradient": {
155
+ "canonical": "GradientView"
156
+ },
157
+ "BlurView": {
158
+ "canonical": "Blur"
159
+ },
160
+ "WebView": {
161
+ "canonical": "Web"
162
+ },
163
+ "Iframe": {
164
+ "canonical": "Web"
165
+ }
166
+ }
167
+ }
@@ -2,7 +2,7 @@ import { execFile } from "child_process";
2
2
  export async function runCli(command, args, options) {
3
3
  const timeout = options.timeout ?? 60_000;
4
4
  return new Promise((resolve) => {
5
- execFile(command, args, {
5
+ const child = execFile(command, args, {
6
6
  cwd: options.cwd,
7
7
  timeout,
8
8
  maxBuffer: 10 * 1024 * 1024,
@@ -22,6 +22,12 @@ export async function runCli(command, args, options) {
22
22
  stderr: stderr || "",
23
23
  });
24
24
  });
25
+ // Nothing is ever written to the child's stdin: end it now, so a CLI that
26
+ // reads stdin — an overwrite prompt — sees end-of-file instead of waiting
27
+ // for a line that never comes. Through 2.13.1 the pipe stayed open, and such
28
+ // a child waited until the timeout above killed it (`jui g converter` on
29
+ // an existing component, measured 2026-09-26).
30
+ child.stdin?.end();
25
31
  });
26
32
  }
27
33
  export function formatResult(result) {
@@ -58,6 +58,13 @@ export interface ReloadRecord {
58
58
  ok: boolean;
59
59
  error?: string;
60
60
  }
61
+ export interface TypeSynonym {
62
+ spelling: string;
63
+ canonical: string;
64
+ renderAs?: string;
65
+ implies?: Record<string, unknown>;
66
+ source: string;
67
+ }
61
68
  export interface DataSourceInfo {
62
69
  attributeDefinitions: FileInfo;
63
70
  componentMetadata: FileInfo;
@@ -66,6 +73,7 @@ export interface DataSourceInfo {
66
73
  attributeSemantics: FileInfo | null;
67
74
  platformSemantics: FileInfo | null;
68
75
  coverage: FileInfo | null;
76
+ typeSynonyms: FileInfo | null;
69
77
  componentCount: number;
70
78
  commonAttributeCount: number;
71
79
  }
@@ -92,6 +100,7 @@ export declare class SpecLoader {
92
100
  private screenIdentity;
93
101
  private commonAttributes;
94
102
  private aliasMap;
103
+ private typeSynonyms;
95
104
  private metadata;
96
105
  private dataSource;
97
106
  /** When this process read the files. Data is cached in memory from then
@@ -156,6 +165,22 @@ export declare class SpecLoader {
156
165
  listComponents(): string[];
157
166
  getDataSource(): DataSourceInfo;
158
167
  private load;
168
+ /**
169
+ * Type spellings the build accepts for a section they are not
170
+ * (shared/core/type_synonyms.json, jsonui-cli 1.9.0 — the one table the
171
+ * tools and the dynamic runtimes read). Until then lookup_component knew
172
+ * only `_alias_of` sections and metadata aliases, and answered "not found"
173
+ * for `ProgressBar`, which the build draws as a Progress.
174
+ *
175
+ * A section, an `_alias_of` section or a metadata alias of the same
176
+ * spelling wins: the exact match is found first, as in the tools. An entry
177
+ * whose canonical is not a section here is skipped.
178
+ *
179
+ * Absent (older jsonui-cli): no synonyms. Present but not JSON: the load
180
+ * throws, so an automatic reload keeps the last content that parsed rather
181
+ * than swapping in a loader that silently lost every synonym.
182
+ */
183
+ private loadTypeSynonyms;
159
184
  private normalizeMetadata;
160
185
  private getMetadata;
161
186
  private buildComponentSpec;
@@ -59,6 +59,10 @@ export class SpecLoader {
59
59
  screenIdentity = null;
60
60
  commonAttributes = null;
61
61
  aliasMap = new Map();
62
+ /// Lower-cased spelling -> its entry in type_synonyms.json, for the
63
+ /// spellings that resolve through it (not a section, `_alias_of` or
64
+ /// metadata alias of their own).
65
+ typeSynonyms = new Map();
62
66
  metadata = {};
63
67
  dataSource;
64
68
  /** When this process read the files. Data is cached in memory from then
@@ -123,6 +127,13 @@ export class SpecLoader {
123
127
  const comp = this.getComponent(name);
124
128
  if (!comp)
125
129
  return null;
130
+ // Say which spelling was asked for and why it resolved: the build
131
+ // accepts `ProgressBar` as a Progress, and an HStack is a View that
132
+ // also means `orientation: horizontal`.
133
+ const synonym = this.typeSynonyms.get(name.toLowerCase());
134
+ if (synonym) {
135
+ return { ...comp, typeSynonym: synonym, commonAttributes: this.commonAttributes };
136
+ }
126
137
  return { ...comp, commonAttributes: this.commonAttributes };
127
138
  }
128
139
  getAttribute(name) {
@@ -202,6 +213,15 @@ export class SpecLoader {
202
213
  matches.push(`rule: ${rule}`);
203
214
  }
204
215
  }
216
+ // A spelling the build accepts for this type (type_synonyms.json).
217
+ for (const synonym of this.typeSynonyms.values()) {
218
+ if (this.getComponent(synonym.spelling) !== comp)
219
+ continue;
220
+ if (synonym.spelling.toLowerCase().includes(q)) {
221
+ score += 8;
222
+ matches.push(`type synonym: ${synonym.spelling}`);
223
+ }
224
+ }
205
225
  if (score > 0)
206
226
  results.push({ component: comp.name, score, matches });
207
227
  }
@@ -290,6 +310,7 @@ export class SpecLoader {
290
310
  this.dataSource.attributeSemantics,
291
311
  this.dataSource.platformSemantics,
292
312
  this.dataSource.coverage,
313
+ this.dataSource.typeSynonyms,
293
314
  ].filter((f) => f != null);
294
315
  const changed = [];
295
316
  for (const file of tracked) {
@@ -458,6 +479,7 @@ export class SpecLoader {
458
479
  }
459
480
  componentCount++;
460
481
  }
482
+ const typeSynonymsResolution = this.loadTypeSynonyms();
461
483
  this.dataSource = {
462
484
  attributeDefinitions: attrResolution,
463
485
  componentMetadata: metaResolution,
@@ -466,10 +488,59 @@ export class SpecLoader {
466
488
  attributeSemantics: attributeSemanticsResolution,
467
489
  platformSemantics: platformSemanticsResolution,
468
490
  coverage: coverageResolution,
491
+ typeSynonyms: typeSynonymsResolution,
469
492
  componentCount,
470
493
  commonAttributeCount: Object.keys(this.commonAttributesRaw).filter((k) => !k.startsWith("_")).length,
471
494
  };
472
495
  }
496
+ /**
497
+ * Type spellings the build accepts for a section they are not
498
+ * (shared/core/type_synonyms.json, jsonui-cli 1.9.0 — the one table the
499
+ * tools and the dynamic runtimes read). Until then lookup_component knew
500
+ * only `_alias_of` sections and metadata aliases, and answered "not found"
501
+ * for `ProgressBar`, which the build draws as a Progress.
502
+ *
503
+ * A section, an `_alias_of` section or a metadata alias of the same
504
+ * spelling wins: the exact match is found first, as in the tools. An entry
505
+ * whose canonical is not a section here is skipped.
506
+ *
507
+ * Absent (older jsonui-cli): no synonyms. Present but not JSON: the load
508
+ * throws, so an automatic reload keeps the last content that parsed rather
509
+ * than swapping in a loader that silently lost every synonym.
510
+ */
511
+ loadTypeSynonyms() {
512
+ this.typeSynonyms = new Map();
513
+ let resolution;
514
+ try {
515
+ resolution = this.resolveFile("shared/core/type_synonyms.json", "data/type_synonyms.json");
516
+ }
517
+ catch {
518
+ return null;
519
+ }
520
+ const doc = JSON.parse(readFileSync(resolution.path, "utf-8"));
521
+ for (const [spelling, entry] of Object.entries(doc.synonyms ?? {})) {
522
+ if (typeof entry !== "object" || entry === null)
523
+ continue;
524
+ const { canonical, render_as: renderAs, ...implies } = entry;
525
+ if (typeof canonical !== "string" || canonical.length === 0)
526
+ continue;
527
+ const key = spelling.toLowerCase();
528
+ if (this.components.has(key) || this.aliasMap.has(key))
529
+ continue;
530
+ const target = this.aliasMap.get(canonical.toLowerCase()) ?? canonical.toLowerCase();
531
+ if (!this.components.has(target))
532
+ continue;
533
+ this.aliasMap.set(key, target);
534
+ this.typeSynonyms.set(key, {
535
+ spelling,
536
+ canonical,
537
+ ...(typeof renderAs === "string" ? { renderAs } : {}),
538
+ ...(Object.keys(implies).length > 0 ? { implies } : {}),
539
+ source: "shared/core/type_synonyms.json",
540
+ });
541
+ }
542
+ return resolution;
543
+ }
473
544
  normalizeMetadata(raw) {
474
545
  return {
475
546
  description: raw.description ?? "",
@@ -6,7 +6,10 @@ export function register(server, config) {
6
6
  from_spec: z.string().optional().describe("Generate from component spec file path"),
7
7
  all: z.boolean().optional().describe("Generate from all component specs"),
8
8
  attributes: z.string().optional().describe("Attributes in key:type format (e.g., 'prop1:String,prop2:Int')"),
9
- container: z.boolean().optional().describe("Mark as container component"),
9
+ container: z.boolean().optional().describe("true: a container (it draws its children). false: a leaf (it takes no " +
10
+ "children, and a layout that gives it some is refused by name) — needs " +
11
+ "jsonui-cli 1.9.0 or later (earlier `jui` rejects --no-container). " +
12
+ "Absent: the default form."),
10
13
  skip_existing: z.boolean().optional().describe("Leave existing converter and scaffold files untouched, without the " +
11
14
  "overwrite prompt — for idempotent scaffold runs. Wins over `force` " +
12
15
  "when both are set."),
@@ -31,9 +34,14 @@ export function register(server, config) {
31
34
  if (params.attributes) {
32
35
  args.push("--attributes", params.attributes);
33
36
  }
34
- if (params.container) {
37
+ // `false` asks for a leaf. It used to pass nothing, so the command ran
38
+ // in the default form and the request was dropped without a word.
39
+ if (params.container === true) {
35
40
  args.push("--container");
36
41
  }
42
+ if (params.container === false) {
43
+ args.push("--no-container");
44
+ }
37
45
  if (params.skip_existing) {
38
46
  args.push("--skip-existing");
39
47
  }
@@ -1,9 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import { runCli, formatResult } from "../../cli_runner.js";
3
3
  export function register(server, config) {
4
- server.tool("jui_generate_project", "Generate Layout JSON and ViewModel files from screen specs (all or single spec)", {
4
+ server.tool("jui_generate_project", "Generate Layout JSON and ViewModel files from screen specs (all or single spec). " +
5
+ "A Layout JSON it generates — a screen's, or a Collection cell / header / footer's — is " +
6
+ "written when it is not there, left as it is when it already holds exactly what the spec " +
7
+ "generates, and otherwise KEPT (the Layout JSON is hand-edited after generation) and named " +
8
+ "in the output with \"Kept existing … layout: <file> … --force replaces it\". From " +
9
+ "jsonui-cli 1.9.0; an earlier jui rewrote an existing layout on every run.", {
5
10
  spec_file: z.string().optional().describe("Single spec file to process (e.g., 'login.spec.json')"),
6
- force: z.boolean().optional().describe("Force overwrite declaration files"),
11
+ force: z.boolean().optional().describe("Replace the ViewModel declaration files, and each Layout JSON that already exists and " +
12
+ "differs from what the spec generates (kept and named without it). A layout whose data " +
13
+ "section holds entries the spec does not declare is not replaced even with force — the " +
14
+ "output names them. Called from here the command has no terminal, so it never asks: " +
15
+ "without force an existing layout is kept."),
7
16
  skip_layout: z.boolean().optional().describe("Skip Layout JSON generation"),
8
17
  dry_run: z.boolean().optional().describe("Show what would be generated without writing"),
9
18
  platform: z.enum(["ios", "android", "web"]).optional().describe("Generate for single platform only"),