mxrb 0.1.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.
Files changed (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
data/bin/mxrb ADDED
@@ -0,0 +1,1613 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/mxrb"
5
+ require "shellwords"
6
+ require "io/console"
7
+
8
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
9
+ def render_plan_report(report, json:)
10
+ payload = {
11
+ engine: report.engine, analyzed: report.analyzed, total_cost: report.total_cost,
12
+ planning_time_ms: report.planning_time_ms, execution_time_ms: report.execution_time_ms,
13
+ clean: report.clean?, findings: report.findings.map(&:to_h)
14
+ }
15
+ return puts(JSON.pretty_generate(payload)) if json
16
+
17
+ puts "Engine : #{report.engine}"
18
+ puts "Mode : #{report.analyzed ? 'actual' : 'estimated'}"
19
+ puts "Total cost : #{report.total_cost}"
20
+ report.findings.each do |finding|
21
+ puts "[#{finding.severity.to_s.upcase}] #{finding.rule}: #{finding.relation || finding.node_type}"
22
+ puts " #{finding.message}"
23
+ puts " #{finding.suggestion}"
24
+ end
25
+ puts "[mxrb] No plan findings" if report.findings.empty?
26
+ end
27
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
28
+
29
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
30
+ def render_workload_report(report, json:)
31
+ payload = {
32
+ engine: report.engine, clean: report.clean?,
33
+ queries: report.queries.map(&:to_h), table_stats: report.table_stats,
34
+ index_stats: report.index_stats, findings: report.findings.map(&:to_h)
35
+ }
36
+ return puts(JSON.pretty_generate(payload)) if json
37
+
38
+ report.queries.each do |query|
39
+ puts "#{query.query_id}\t#{query.calls} calls\t#{query.total_time_ms.round(3)} ms total\t" \
40
+ "#{query.mean_time_ms.round(3)} ms mean"
41
+ end
42
+ report.findings.each do |finding|
43
+ puts "[#{finding.severity.to_s.upcase}] #{finding.rule}: #{finding.subject}"
44
+ puts " #{finding.message}"
45
+ puts " #{finding.suggestion}"
46
+ end
47
+ puts "[mxrb] No workload findings" if report.findings.empty?
48
+ end
49
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
50
+
51
+ def render_index_advice(advice, json:) # rubocop:disable Metrics/AbcSize
52
+ payload = {
53
+ candidates: advice.candidates.map(&:to_h), redundant_indexes: advice.redundant_indexes
54
+ }
55
+ return puts(JSON.pretty_generate(payload)) if json
56
+
57
+ advice.candidates.each do |candidate|
58
+ puts "[#{candidate.confidence.to_s.upcase}] #{candidate.relation}(#{candidate.columns.join(', ')})"
59
+ puts " #{candidate.reason} Queries: #{candidate.query_ids.join(', ')}"
60
+ end
61
+ advice.redundant_indexes.each { puts "[WARNING] overlapping indexes: #{_1.join(' and ')}" }
62
+ puts '[mxrb] No evidence-backed index candidates' if advice.candidates.empty?
63
+ end
64
+
65
+ SCAFFOLD_COMMANDS = (Mxrb::Scaffold::Help::COMMANDS.keys - ["design"]).freeze
66
+
67
+ command = ARGV.shift
68
+
69
+ case command
70
+ when 'doctor'
71
+ json = ARGV.delete('--json')
72
+ root = ARGV.shift || Dir.pwd
73
+ report = Mxrb::Doctor.new(root).run
74
+ if json
75
+ puts JSON.pretty_generate(root: report.root, valid: report.valid?, checks: report.checks.map(&:to_h))
76
+ else
77
+ report.checks.each { puts "[#{_1.status.to_s.upcase}] #{_1.name}: #{_1.message}" }
78
+ puts "[mxrb] #{report.errors.size} error(s), #{report.warnings.size} warning(s)"
79
+ end
80
+ exit 1 unless report.valid?
81
+
82
+ when 'benchmark'
83
+ path = ARGV.shift or abort 'Usage: mxrb benchmark <file.mpr> [--iterations N] [--json]'
84
+ index = ARGV.index('--iterations')
85
+ iterations = index ? ARGV.slice!(index, 2).last : 3
86
+ abort '--iterations requires a value' if index && iterations.nil?
87
+ json = ARGV.delete('--json')
88
+ result = Mxrb::Benchmark.new(path, iterations:).run
89
+ if json
90
+ puts JSON.pretty_generate(result.to_h)
91
+ else
92
+ puts "Units : #{result.units}"
93
+ puts "Open average : #{result.open_seconds}s"
94
+ puts "Index average : #{result.index_seconds}s"
95
+ puts "Validate average : #{result.validate_seconds}s"
96
+ end
97
+
98
+ when 'project'
99
+ action = ARGV.shift or abort 'Usage: mxrb project inspect [DIR] [--json]'
100
+ abort "Unknown project action #{action.inspect}" unless action == 'inspect'
101
+ json = ARGV.delete('--json')
102
+ payload = Mxrb::ProjectLifecycle.new(ARGV.shift || Dir.pwd).inspect
103
+ if json
104
+ puts JSON.pretty_generate(payload)
105
+ else
106
+ payload.each { |key, value| puts "#{key}: #{Array(value).join(', ')}" }
107
+ end
108
+
109
+ when 'upgrade'
110
+ version_index = ARGV.index('--mendix')
111
+ abort 'Usage: mxrb upgrade --mendix VERSION [--target DIR] [--apply]' unless version_index
112
+ version = ARGV.slice!(version_index, 2).last
113
+ abort '--mendix requires a value' unless version
114
+ target_index = ARGV.index('--target')
115
+ target = target_index ? ARGV.slice!(target_index, 2).last : Dir.pwd
116
+ abort '--target requires a value' if target_index && target.nil?
117
+ apply = ARGV.delete('--apply') ? true : false
118
+ result = Mxrb::ProjectLifecycle.new(target).upgrade(version, apply:)
119
+ puts "[mxrb] #{apply ? 'Updated' : 'Preview'} #{result.path}: #{result.from} -> #{result.to}"
120
+
121
+ when 'migrate'
122
+ action = ARGV.shift or abort 'Usage: mxrb migrate <check|plan> [DIR] [--json]'
123
+ json = ARGV.delete('--json')
124
+ result = Mxrb::ProjectLifecycle.new(ARGV.shift || Dir.pwd).migration_plan
125
+ payload = {
126
+ clean: result.clean?, added: result.diff.added.map(&:to_h),
127
+ removed: result.diff.removed.map(&:to_h), changed: result.diff.changed.map(&:to_h)
128
+ }
129
+ if json
130
+ puts JSON.pretty_generate(payload)
131
+ else
132
+ puts "Added: #{payload[:added].size}"
133
+ puts "Removed: #{payload[:removed].size}"
134
+ puts "Changed: #{payload[:changed].size}"
135
+ puts "[mxrb] #{result.clean? ? 'No model drift' : 'Model drift detected'}"
136
+ end
137
+ exit 1 if action == 'check' && !result.clean?
138
+ abort "Unknown migrate action #{action.inspect}" unless %w[check plan].include?(action)
139
+
140
+ when "inspect"
141
+ path = ARGV.shift or abort "Usage: mxrb inspect <file.mpr>"
142
+ Mxrb.open(path) do |p|
143
+ puts "Project : #{p.name}"
144
+ puts "Version : #{p.mendix_version}"
145
+ puts "Tables : #{p.tables.join(', ')}"
146
+ puts ""
147
+ puts "Unit types found:"
148
+ p.all_units.filter_map { p.parse_bson(_1)["$Type"] }.sort.uniq.each { puts " #{_1}" }
149
+ puts ""
150
+ puts "All units:"
151
+ p.all_units.each do |u|
152
+ type = p.parse_bson(u)["$Type"]
153
+ puts " [#{u['UnitID']}] #{type} (container=#{u['ContainerID']}, name=#{u['ContainmentName']})"
154
+ end
155
+ end
156
+
157
+ when "dump-unit"
158
+ path = ARGV.shift or abort "Usage: mxrb dump-unit <file.mpr> <unit_id>"
159
+ id = ARGV.shift or abort "Usage: mxrb dump-unit <file.mpr> <unit_id>"
160
+ Mxrb.open(path) do |p|
161
+ u = p.raw_unit(id)
162
+ abort "Unit #{id} not found" unless u
163
+ puts "UnitID : #{u['UnitID']}"
164
+ puts "ContainerID : #{u['ContainerID']}"
165
+ puts "ContainmentName : #{u['ContainmentName']}"
166
+ puts "TypeName : #{p.parse_bson(u)['$Type']}"
167
+ puts "ContentsHash : #{u['ContentsHash']}"
168
+ puts "Contents (hex) :"
169
+ blob = p.mpr.content_bytes(u)
170
+ if blob
171
+ blob.b.bytes.each_slice(16).with_index do |row, i|
172
+ hex = row.map { "%02x" % _1 }.join(" ")
173
+ asc = row.map { _1.between?(32, 126) ? _1.chr : "." }.join
174
+ printf " %04x %-48s %s\n", i * 16, hex, asc
175
+ end
176
+ else
177
+ puts " (empty)"
178
+ end
179
+ end
180
+
181
+ when "sql"
182
+ path = ARGV.shift or abort "Usage: mxrb sql <file.mpr> \"SELECT ...\""
183
+ sql = ARGV.shift or abort "Usage: mxrb sql <file.mpr> \"SELECT ...\""
184
+ Mxrb.open(path) do |p|
185
+ rows = p.query(sql)
186
+ rows.each { puts _1.inspect }
187
+ puts "(#{rows.size} rows)"
188
+ end
189
+
190
+ when "modules"
191
+ path = ARGV.shift or abort "Usage: mxrb modules <file.mpr>"
192
+ Mxrb.open(path) do |p|
193
+ p.modules.each { puts _1.inspect }
194
+ end
195
+
196
+ when "protocols"
197
+ path = ARGV.shift or abort "Usage: mxrb protocols <file.mpr> [--json]"
198
+ json = ARGV.delete("--json")
199
+ Mxrb.open(path) do |project|
200
+ audit = Mxrb::Protocols.audit(project)
201
+ unknown = audit.unknown_marketplace_modules
202
+ if json
203
+ puts JSON.generate(
204
+ connectors: audit.connectors.map(&:to_h),
205
+ unknown_marketplace_modules: unknown,
206
+ project: File.basename(path)
207
+ )
208
+ else
209
+ if audit.connectors.empty?
210
+ puts "No known protocol connectors found."
211
+ else
212
+ audit.connectors.each do |connector|
213
+ puts "#{connector.protocol}\t#{connector.module_name}\t" \
214
+ "marketplace=#{connector.metadata[:marketplace_id]}\tprotected=#{connector.protected}"
215
+ puts " entities: #{connector.entities.join(', ')}" unless connector.entities.empty?
216
+ puts " microflows: #{connector.microflows.join(', ')}" unless connector.microflows.empty?
217
+ end
218
+ end
219
+ unless unknown.empty?
220
+ puts "Unrecognized marketplace modules (#{unknown.size}): #{unknown.join(', ')}"
221
+ puts "Run `mxrb modules #{path}` to list every module."
222
+ end
223
+ end
224
+ end
225
+
226
+ when "generate"
227
+ source = ARGV.shift or abort "Usage: mxrb generate <definition.rb> [file.mpr]"
228
+ target = ARGV.shift
229
+ abort "#{source}: file not found" unless File.file?(source)
230
+ if target
231
+ ENV["MXRB_OUTPUT_PATH"] = File.expand_path(target)
232
+ end
233
+ load File.expand_path(source)
234
+ puts "[mxrb] Generated #{target || 'project from definition'}"
235
+
236
+ when "widgets"
237
+ widgets_usage = <<~USAGE
238
+ Usage: mxrb widgets sync <definition.rb> <file.mpr>
239
+
240
+ Generate the project, load schemas from .mpk files in widgets/, and
241
+ regenerate Data Grid 2, Combo Box, and other pluggable widget properties.
242
+ USAGE
243
+ action = ARGV.shift
244
+ if action.nil? || %w[-h --help help].include?(action)
245
+ puts widgets_usage
246
+ exit(action.nil? ? 1 : 0)
247
+ end
248
+ abort "Unknown widgets action #{action.inspect}; use sync" unless action == "sync"
249
+ if %w[-h --help].include?(ARGV.first)
250
+ puts widgets_usage
251
+ exit 0
252
+ end
253
+ source = ARGV.shift or abort "Usage: mxrb widgets sync <definition.rb> <file.mpr>"
254
+ target = ARGV.shift or abort "Usage: mxrb widgets sync <definition.rb> <file.mpr>"
255
+ result = Mxrb::WidgetSynchronizer.new(source, target).sync!
256
+ puts "[mxrb] Synchronized widget schemas with #{result.mx_path}"
257
+ puts "[mxrb] Generated #{result.project}"
258
+
259
+ when "frontend"
260
+ frontend_usage = <<~USAGE
261
+ Usage: mxrb frontend migrate FILE.mpr [--apply] [--json]
262
+
263
+ Preview frontend widget, layout-row, and design-property migrations.
264
+ The MPR is changed only when --apply is provided and the plan is safe.
265
+ USAGE
266
+ action = ARGV.shift
267
+ if action.nil? || %w[-h --help help].include?(action)
268
+ puts frontend_usage
269
+ exit(action.nil? ? 1 : 0)
270
+ end
271
+ abort "Unknown frontend action #{action.inspect}; use migrate" unless action == "migrate"
272
+ if %w[-h --help].include?(ARGV.first)
273
+ puts frontend_usage
274
+ exit 0
275
+ end
276
+ path = ARGV.shift or abort "Usage: mxrb frontend migrate FILE.mpr [--apply] [--json]"
277
+ apply = !ARGV.delete("--apply").nil?
278
+ json = !ARGV.delete("--json").nil?
279
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
280
+ plan = Mxrb::Frontend::Migrator.plan(path)
281
+ plan.apply! if apply && plan.safe?
282
+ payload = {
283
+ version: plan.version, changes: plan.changes.size, widgets: plan.widgets,
284
+ layout_rows: plan.layout_rows, design_properties: plan.design_properties,
285
+ issues: plan.issues.map(&:to_h), safe: plan.safe?, applied: plan.applied?
286
+ }
287
+ if json
288
+ puts JSON.pretty_generate(payload)
289
+ else
290
+ puts "Version : #{payload[:version]}"
291
+ puts "Changes : #{payload[:changes]}"
292
+ puts "Widgets : #{payload[:widgets]}"
293
+ puts "Layout rows : #{payload[:layout_rows]}"
294
+ puts "Design properties : #{payload[:design_properties]}"
295
+ puts "Safe : #{payload[:safe]}"
296
+ puts "Applied : #{payload[:applied]}"
297
+ plan.issues.each do |issue|
298
+ puts "[#{issue.kind.to_s.upcase}] #{issue.unit_id}#{issue.path}: #{issue.message}"
299
+ end
300
+ end
301
+ exit 1 unless plan.safe?
302
+
303
+ when "export"
304
+ source = ARGV.shift or abort "Usage: mxrb export <file.mpr> <ruby-project-dir>"
305
+ output = ARGV.shift or abort "Usage: mxrb export <file.mpr> <ruby-project-dir>"
306
+ destination = Mxrb::Exporter.new(source, output).export!
307
+ puts "[mxrb] Exported Ruby project to #{destination}"
308
+
309
+ when "validate"
310
+ path = ARGV.shift or abort "Usage: mxrb validate <file.mpr>"
311
+ result = Mxrb.validate(path)
312
+ result.warnings.each { warn "[mxrb] warning: #{_1}" }
313
+ if result.valid?
314
+ puts "[mxrb] OK"
315
+ else
316
+ result.errors.each { warn "[mxrb] error: #{_1}" }
317
+ exit 1
318
+ end
319
+
320
+ when "preflight"
321
+ path = ARGV.shift or abort "Usage: mxrb preflight <file.mpr> [--json]"
322
+ json = ARGV.delete("--json")
323
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
324
+ report = Mxrb.compatibility(path)
325
+ if json
326
+ puts JSON.pretty_generate(report.to_h)
327
+ else
328
+ puts "Project : #{report.path}"
329
+ puts "Mendix : #{report.mendix_version}"
330
+ puts "Units : #{report.stats[:units]}"
331
+ report.findings.each do |finding|
332
+ puts "[#{finding.severity.to_s.upcase}] #{finding.category} #{finding.type} " \
333
+ "at #{finding.location}: #{finding.message}"
334
+ end
335
+ puts "[mxrb] #{report.errors.size} error(s), #{report.warnings.size} warning(s)"
336
+ end
337
+ exit 1 unless report.compatible?
338
+
339
+ when "pack"
340
+ path = ARGV.shift or abort "Usage: mxrb pack <file.mpr> --output FILE.mda [--deployment DIR] [--force]"
341
+ option = lambda do |name|
342
+ index = ARGV.index(name)
343
+ next unless index
344
+
345
+ abort "#{name} requires a value" unless ARGV[index + 1]
346
+ ARGV.slice!(index, 2).last
347
+ end
348
+ output = option.call("--output") || File.join(
349
+ File.dirname(File.expand_path(path)), "build", "#{File.basename(path, '.mpr')}.mda"
350
+ )
351
+ deployment = option.call("--deployment")
352
+ force = ARGV.delete("--force") ? true : false
353
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
354
+ result = Mxrb::Compiler::Packager.new(path, deployment:).pack(output:, force:)
355
+ puts "[mxrb] Packed #{result.files} files for Mendix #{result.mendix_version}"
356
+ puts "[mxrb] SHA-256 #{result.sha256}"
357
+ puts "[mxrb] #{result.path}"
358
+
359
+ when "portable"
360
+ path = ARGV.shift or abort "Usage: mxrb portable <file.mpr> --output runtime.zip [options]"
361
+ option = lambda do |name|
362
+ index = ARGV.index(name)
363
+ next unless index
364
+
365
+ abort "#{name} requires a value" unless ARGV[index + 1]
366
+ ARGV.slice!(index, 2).last
367
+ end
368
+ output = option.call("--output") || File.join(
369
+ File.dirname(File.expand_path(path)), "build", "runtime.zip"
370
+ )
371
+ deployment = option.call("--deployment")
372
+ mendix_home = option.call("--mendix-home")
373
+ force = ARGV.delete("--force") ? true : false
374
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
375
+ result = Mxrb::Compiler::PortablePackager.new(
376
+ path, deployment:, mendix_home:
377
+ ).pack(output:, force:)
378
+ puts "[mxrb] Packed portable Runtime with #{result.files} files for Mendix #{result.mendix_version}"
379
+ puts "[mxrb] SHA-256 #{result.sha256}"
380
+ puts "[mxrb] #{result.path}"
381
+
382
+ when "mda"
383
+ action = ARGV.shift or abort "Usage: mxrb mda <inspect|compare> ..."
384
+ case action
385
+ when "inspect"
386
+ path = ARGV.shift or abort "Usage: mxrb mda inspect <file.mda> [--json]"
387
+ json = ARGV.delete("--json")
388
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
389
+ result = Mxrb::Compiler::Mda.inspect(path)
390
+ if json
391
+ puts JSON.pretty_generate(
392
+ path: result.path, sha256: result.sha256, roots: result.roots,
393
+ files: result.files.size, metadata: result.metadata
394
+ )
395
+ else
396
+ puts "Runtime : #{result.metadata['RuntimeVersion']}"
397
+ puts "Project : #{result.metadata['ProjectName']}"
398
+ puts "Files : #{result.files.size}"
399
+ puts "Roots : #{result.roots.join(', ')}"
400
+ puts "SHA-256 : #{result.sha256}"
401
+ end
402
+ when "compare"
403
+ left = ARGV.shift or abort "Usage: mxrb mda compare <left.mda> <right.mda>"
404
+ right = ARGV.shift or abort "Usage: mxrb mda compare <left.mda> <right.mda>"
405
+ differences = Mxrb::Compiler::Mda.compare(left, right)
406
+ differences.each { puts "#{_1.status}\t#{_1.path}" }
407
+ puts "[mxrb] #{differences.size} difference(s)"
408
+ else
409
+ abort "Unknown mda action #{action.inspect}; use inspect or compare"
410
+ end
411
+
412
+ when "compare"
413
+ left = ARGV.shift or abort "Usage: mxrb compare <left.mpr> <right.mpr>"
414
+ right = ARGV.shift or abort "Usage: mxrb compare <left.mpr> <right.mpr>"
415
+ result = Mxrb.compare(left, right)
416
+ if result.identical?
417
+ puts "[mxrb] OK"
418
+ else
419
+ result.differences.each { puts "[mxrb] diff: #{_1}" }
420
+ exit 1
421
+ end
422
+
423
+ when "diff"
424
+ left = ARGV.shift or abort "Usage: mxrb diff <left.mpr> <right.mpr>"
425
+ right = ARGV.shift or abort "Usage: mxrb diff <left.mpr> <right.mpr>"
426
+ result = Mxrb.diff(left, right)
427
+ result.changes.each do |change|
428
+ puts "#{change.operation}\t#{change.path.join('.')}\t" \
429
+ "#{change.before.inspect}\t=>\t#{change.after.inspect}"
430
+ end
431
+ exit 1 unless result.identical?
432
+
433
+ when "refs", "callers", "callees", "impact"
434
+ path = ARGV.shift or abort "Usage: mxrb #{command} <file.mpr> <qualified-name>"
435
+ name = ARGV.shift or abort "Usage: mxrb #{command} <file.mpr> <qualified-name>"
436
+ begin
437
+ Mxrb.open(path) do |project|
438
+ case command
439
+ when "refs"
440
+ project.references_to(name).each do |reference|
441
+ puts [
442
+ reference.source.qualified_name,
443
+ reference.source.kind,
444
+ reference.relation,
445
+ reference.path.join(".")
446
+ ].join("\t")
447
+ end
448
+ when "callers"
449
+ project.callers_of(name).each do |artifact|
450
+ puts "#{artifact.qualified_name}\t#{artifact.kind}"
451
+ end
452
+ when "callees"
453
+ project.callees_of(name).each do |artifact|
454
+ puts "#{artifact.qualified_name}\t#{artifact.kind}"
455
+ end
456
+ when "impact"
457
+ project.impact_of(name).artifacts.each do |artifact|
458
+ puts "#{artifact.qualified_name}\t#{artifact.kind}"
459
+ end
460
+ end
461
+ end
462
+ rescue KeyError, ArgumentError => e
463
+ warn "[mxrb] error: #{e.message}"
464
+ exit 1
465
+ end
466
+
467
+ when "rename"
468
+ path = ARGV.shift or abort "Usage: mxrb rename <file.mpr> <old-name> <new-name> [--apply]"
469
+ old_name = ARGV.shift or abort "Usage: mxrb rename <file.mpr> <old-name> <new-name> [--apply]"
470
+ new_name = ARGV.shift or abort "Usage: mxrb rename <file.mpr> <old-name> <new-name> [--apply]"
471
+ apply = ARGV.delete("--apply")
472
+ begin
473
+ plan = Mxrb.open(path, readonly: !apply) do |project|
474
+ project.plan_rename(old_name, to: new_name).tap { _1.apply! if apply }
475
+ end
476
+ rescue KeyError, ArgumentError => e
477
+ warn "[mxrb] error: #{e.message}"
478
+ exit 1
479
+ end
480
+ plan.changes.each do |change|
481
+ puts [
482
+ change.unit_id,
483
+ change.path.join("."),
484
+ change.before.inspect,
485
+ "=>",
486
+ change.after.inspect
487
+ ].join("\t")
488
+ end
489
+ puts "[mxrb] #{apply ? 'Renamed' : 'Preview'}: #{plan.changes.size} changes " \
490
+ "in #{plan.affected_units.size} units"
491
+
492
+ when "remove"
493
+ path = ARGV.shift or abort "Usage: mxrb remove <file.mpr> <qualified-name> [--apply]"
494
+ name = ARGV.shift or abort "Usage: mxrb remove <file.mpr> <qualified-name> [--apply]"
495
+ apply = ARGV.delete("--apply")
496
+ begin
497
+ plan = Mxrb.open(path, readonly: !apply) do |project|
498
+ project.plan_remove(name).tap { _1.apply! if apply }
499
+ end
500
+ rescue KeyError, ArgumentError => e
501
+ warn "[mxrb] error: #{e.message}"
502
+ exit 1
503
+ end
504
+ puts "Artifact : #{plan.artifact.qualified_name} (#{plan.artifact.kind})"
505
+ puts "Incoming references : #{plan.incoming.size}"
506
+ plan.incoming.each { puts " #{_1.source.qualified_name}\t#{_1.relation}" }
507
+ puts "Child units : #{plan.children.size}"
508
+ puts "[mxrb] #{apply ? 'Removed' : plan.safe? ? 'Safe removal preview' : 'Blocked removal'}"
509
+ exit 1 unless plan.safe?
510
+
511
+ when "move"
512
+ path = ARGV.shift or abort "Usage: mxrb move <file.mpr> <name> <container> [--apply]"
513
+ name = ARGV.shift or abort "Usage: mxrb move <file.mpr> <name> <container> [--apply]"
514
+ container = ARGV.shift or abort "Usage: mxrb move <file.mpr> <name> <container> [--apply]"
515
+ apply = ARGV.delete("--apply")
516
+ begin
517
+ plan = Mxrb.open(path, readonly: !apply) do |project|
518
+ project.plan_move(name, to: container).tap { _1.apply! if apply }
519
+ end
520
+ rescue KeyError, ArgumentError => e
521
+ warn "[mxrb] error: #{e.message}"
522
+ exit 1
523
+ end
524
+ puts "Artifact : #{plan.source.qualified_name} (#{plan.source.kind})"
525
+ puts "Container : #{plan.before_container} -> #{plan.after_container}"
526
+ puts "[mxrb] #{apply ? 'Moved' : plan.empty? ? 'Already in container' : 'Move preview'}"
527
+
528
+ when "lint", "report"
529
+ path = ARGV.shift or abort "Usage: mxrb #{command} <file.mpr>"
530
+ report = Mxrb.open(path, &:analyze)
531
+ if command == "lint"
532
+ report.diagnostics.each do |diagnostic|
533
+ puts "[#{diagnostic.severity}] #{diagnostic.rule}: #{diagnostic.message}"
534
+ end
535
+ else
536
+ puts "Artifacts not referenced : #{report.unreferenced.size}"
537
+ puts "Unresolved references : #{report.unresolved_references.size}"
538
+ puts "Call cycles : #{report.call_cycles.size}"
539
+ puts "Cross-module dependencies: #{report.module_dependencies.size}"
540
+ report.module_dependencies.each do |dependency|
541
+ puts [
542
+ dependency.from,
543
+ "->",
544
+ dependency.to,
545
+ "#{dependency.references.size} references",
546
+ "#{dependency.source_artifacts.size} artifacts"
547
+ ].join("\t")
548
+ end
549
+ end
550
+ puts "[mxrb] #{report.errors.size} errors, #{report.warnings.size} warnings"
551
+ exit 1 unless report.valid?
552
+
553
+ when "cache"
554
+ action = ARGV.shift or abort "Usage: mxrb cache <status|warm|clear> <file.mpr> [--json]"
555
+ path = ARGV.shift or abort "Usage: mxrb cache <status|warm|clear> <file.mpr> [--json]"
556
+ json = ARGV.delete("--json")
557
+ readonly = action == "status"
558
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
559
+ result = Mxrb.open(path, readonly:) do |project|
560
+ case action
561
+ when "status"
562
+ project.semantic_cache_info
563
+ when "warm"
564
+ project.warm_semantic_cache!
565
+ when "clear"
566
+ removed = project.clear_semantic_cache!
567
+ project.semantic_cache_info.merge(removed:)
568
+ else
569
+ abort "Unknown cache action #{action.inspect}; use status, warm or clear"
570
+ end
571
+ end
572
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
573
+ payload = result.merge(elapsed_seconds: elapsed.round(6))
574
+ if json
575
+ puts JSON.pretty_generate(payload)
576
+ else
577
+ puts "Present : #{payload[:present]}"
578
+ puts "Current hit : #{payload[:hit]}"
579
+ puts "Entries : #{payload[:entries]}"
580
+ puts "Bytes : #{payload[:bytes]}"
581
+ puts "Fingerprint : #{payload[:current_fingerprint] || '(unavailable)'}"
582
+ puts "Removed : #{payload[:removed]}" if payload.key?(:removed)
583
+ puts "Elapsed : #{format('%.6f', payload[:elapsed_seconds])}s"
584
+ end
585
+
586
+ when "oql"
587
+ path = ARGV.shift or abort "Usage: mxrb oql <file.mpr> [--dialect ansi|postgresql|sql_server] [--json]"
588
+ json = ARGV.delete("--json")
589
+ dialect_index = ARGV.index("--dialect")
590
+ dialect = if dialect_index
591
+ abort "--dialect requires a value" unless ARGV[dialect_index + 1]
592
+ ARGV.slice!(dialect_index, 2).last.to_sym
593
+ else
594
+ :postgresql
595
+ end
596
+ begin
597
+ views = Mxrb.open(path) { _1.oql_sql_views(dialect:) }
598
+ rescue ArgumentError => e
599
+ abort "[mxrb] error: #{e.message}"
600
+ end
601
+ if json
602
+ puts JSON.pretty_generate(
603
+ views.map do |view|
604
+ {
605
+ name: view.query.qualified_name, kind: view.query.kind, oql: view.query.oql,
606
+ sql: view.sql, dialect: view.dialect, confidence: view.confidence,
607
+ parameters: view.parameters, warnings: view.warnings
608
+ }
609
+ end
610
+ )
611
+ elsif views.empty?
612
+ puts "[mxrb] No native OQL found"
613
+ else
614
+ views.each_with_index do |view, index|
615
+ puts "" unless index.zero?
616
+ puts "#{view.query.qualified_name} (#{view.query.kind})"
617
+ puts "OQL:"
618
+ puts view.query.oql
619
+ puts "SQL (#{view.dialect}, #{view.confidence}):"
620
+ puts(view.sql || "(not generated)")
621
+ view.warnings.each { warn "[mxrb] warning: #{_1}" }
622
+ end
623
+ end
624
+
625
+ when "analyze"
626
+ option = lambda do |name|
627
+ index = ARGV.index(name)
628
+ next unless index
629
+
630
+ abort "#{name} requires a value" unless ARGV[index + 1]
631
+ ARGV.slice!(index, 2).last
632
+ end
633
+ dialect = (option.call("--dialect") || "postgresql").to_sym
634
+ sql_source = option.call("--sql")
635
+ oql_source = option.call("--oql")
636
+ abort "Use only one of --sql or --oql" if sql_source && oql_source
637
+ json = ARGV.delete("--json")
638
+ begin
639
+ analyzer = Mxrb::Oql::Analyzer.new(dialect:)
640
+ reports = if sql_source
641
+ [analyzer.analyze_sql(sql_source)]
642
+ elsif oql_source
643
+ [analyzer.analyze_source(oql_source)]
644
+ else
645
+ path = ARGV.shift or
646
+ abort "Usage: mxrb analyze <file.mpr> [--dialect DIALECT] [--json]"
647
+ Mxrb.open(path) { _1.oql_analysis(dialect:) }
648
+ end
649
+ rescue ArgumentError => e
650
+ abort "[mxrb] error: #{e.message}"
651
+ end
652
+ payload = reports.map do |report|
653
+ {
654
+ name: report.query.qualified_name, kind: report.query.kind, source: report.query.oql,
655
+ clean: report.clean?, warnings: report.warnings?,
656
+ findings: report.findings.map do |finding|
657
+ {
658
+ rule: finding.rule, severity: finding.severity, fragment: finding.fragment,
659
+ message: finding.message, suggestions: finding.suggestions,
660
+ selected_suggestion: finding.suggestions[dialect]
661
+ }
662
+ end
663
+ }
664
+ end
665
+ if json
666
+ puts JSON.pretty_generate(payload)
667
+ elsif payload.all? { _1[:findings].empty? }
668
+ puts "[mxrb] No analysis findings"
669
+ else
670
+ dialect_name = { postgresql: "PostgreSQL", sql_server: "SQL Server", ansi: "ANSI" }
671
+ reports.each do |report|
672
+ report.for_dialect(dialect).each do |finding, suggestion|
673
+ puts "#{report.query.qualified_name} [#{finding.severity.to_s.upcase}] #{finding.rule}"
674
+ puts " #{report.query.kind.to_s.upcase}: #{finding.fragment}"
675
+ puts " #{finding.message}"
676
+ puts " #{dialect_name.fetch(dialect)} -> #{suggestion}"
677
+ end
678
+ end
679
+ end
680
+
681
+ when "design"
682
+ action = ARGV.shift or abort "Usage: mxrb design <scan|migrate> ..."
683
+ if ["init", "--help"].include?(action)
684
+ ARGV.unshift(action)
685
+ Mxrb::Scaffold::CLI.new("design", ARGV).run
686
+ exit 0
687
+ end
688
+ path = ARGV.shift or abort "Usage: mxrb design #{action} <file.mpr> ..."
689
+ json = ARGV.delete("--json")
690
+ case action
691
+ when "scan"
692
+ design = Mxrb.open(path, &:design_system)
693
+ payload = {
694
+ tokens: design.tokens.map do |token|
695
+ {
696
+ name: token.name, value: token.value, kind: token.kind, theme: token.theme,
697
+ path: token.path, line: token.line
698
+ }
699
+ end,
700
+ themes: design.themes, catalogs: design.catalogs,
701
+ unresolved_references: design.unresolved_references.map do |entry|
702
+ { token: entry[:token].name, reference: entry[:reference] }
703
+ end,
704
+ literal_colors: design.literal_colors.map(&:name)
705
+ }
706
+ if json
707
+ puts JSON.pretty_generate(payload)
708
+ else
709
+ puts "name\tvalue\tkind\ttheme\tlocation"
710
+ design.tokens.each do |token|
711
+ puts [
712
+ token.name, token.value, token.kind, token.theme || "-", "#{token.path}:#{token.line}"
713
+ ].join("\t")
714
+ end
715
+ puts "[mxrb] #{design.tokens.size} tokens, #{design.themes.size} themes, " \
716
+ "#{design.literal_colors.size} literal colors, " \
717
+ "#{design.unresolved_references.size} unresolved references"
718
+ end
719
+ when "migrate"
720
+ literal = ARGV.shift or
721
+ abort "Usage: mxrb design migrate <file.mpr> <literal> <token> [--apply] [--json]"
722
+ token = ARGV.shift or
723
+ abort "Usage: mxrb design migrate <file.mpr> <literal> <token> [--apply] [--json]"
724
+ apply = ARGV.delete("--apply")
725
+ plan = Mxrb.open(path) { _1.plan_design_token_migration(literal => token) }
726
+ plan.apply! if apply
727
+ payload = {
728
+ applied: plan.applied?,
729
+ changes: plan.changes.map { { path: _1.path, occurrences: _1.occurrences } },
730
+ occurrences: plan.changes.sum(&:occurrences)
731
+ }
732
+ if json
733
+ puts JSON.pretty_generate(payload)
734
+ else
735
+ plan.changes.each { puts "#{_1.path}\t#{_1.occurrences} replacements" }
736
+ label = apply ? "Applied" : "Preview" # rubocop:disable Metrics/BlockNesting
737
+ puts "[mxrb] #{label}: #{payload[:occurrences]} replacements in #{plan.changes.size} files"
738
+ end
739
+ else
740
+ abort "Unknown design action #{action.inspect}; use scan or migrate"
741
+ end
742
+
743
+ when "db"
744
+ action = ARGV.shift or
745
+ abort "Usage: mxrb db <up|sync|down|destroy|status|url|sql|explain|workload|indexes|shell> <file.mpr>"
746
+ path = ARGV.shift or abort "Usage: mxrb db #{action} <file.mpr>"
747
+ option = lambda do |name, fallback = nil|
748
+ index = ARGV.index(name)
749
+ next fallback unless index
750
+
751
+ abort "#{name} requires a value" unless ARGV[index + 1]
752
+ ARGV.slice!(index, 2).last
753
+ end
754
+ engine = option.call("--engine", "postgresql").to_sym
755
+ port_index = ARGV.index("--port")
756
+ port = if port_index
757
+ abort "--port requires a value" unless ARGV[port_index + 1]
758
+ ARGV.slice!(port_index, 2).last
759
+ else
760
+ 55_432
761
+ end
762
+ runtime_port = option.call("--runtime-port", 18_080)
763
+ if engine == :sql_server
764
+ unless %w[explain workload].include?(action)
765
+ abort "--engine sql_server is supported only by db explain and db workload"
766
+ end
767
+
768
+ workspace = Mxrb::Runtime::SqlServerDatabase.new(
769
+ server: option.call("--server"), database: option.call("--database"),
770
+ user: option.call("--user"), sqlcmd: option.call("--sqlcmd", "sqlcmd")
771
+ )
772
+ elsif engine == :postgresql
773
+ workspace = Mxrb::Runtime::DatabaseWorkspace.new(path, port:, runtime_port:)
774
+ else
775
+ abort "Unsupported database engine #{engine.inspect}; use postgresql or sql_server"
776
+ end
777
+ case action
778
+ when "up", "sync"
779
+ info = action == "sync" ? workspace.sync : workspace.up
780
+ puts "[mxrb] PostgreSQL and Mendix Runtime started"
781
+ puts "Host : #{info.host}:#{info.port}"
782
+ puts "Database : #{info.database}"
783
+ puts "Role : #{info.reader_user} (read-only)"
784
+ puts "Runtime : #{info.runtime_url}"
785
+ when "down"
786
+ workspace.down
787
+ puts "[mxrb] Runtime and PostgreSQL stopped; the data volume was preserved"
788
+ when "destroy"
789
+ abort "db destroy requires --yes" unless ARGV.delete("--yes")
790
+ workspace.destroy(confirm: true)
791
+ puts "[mxrb] Removed the isolated containers, volume, network, and local state"
792
+ when "status"
793
+ info = workspace.status
794
+ puts "Running : #{info.running}"
795
+ puts "Database : #{info.database_container}"
796
+ puts "Runtime : #{info.runtime_container}"
797
+ puts "State : #{info.state_dir}"
798
+ when "url"
799
+ puts workspace.connection_url
800
+ when "sql"
801
+ sql = ARGV.shift or abort "Usage: mxrb db sql <file.mpr> \"SELECT ...\" [--write]"
802
+ write = ARGV.delete("--write")
803
+ puts workspace.query(sql, write:)
804
+ when "explain"
805
+ sql = ARGV.shift or
806
+ abort "Usage: mxrb db explain <file.mpr> \"SELECT ...\" [--analyze] [--json]"
807
+ analyze = ARGV.delete("--analyze") == "--analyze"
808
+ json = ARGV.delete("--json") == "--json"
809
+ report = workspace.explain(sql, analyze:)
810
+ render_plan_report(report, json:)
811
+ when "workload"
812
+ limit = option.call("--limit", 20)
813
+ save = option.call('--save')
814
+ compare = option.call('--compare')
815
+ json = ARGV.delete("--json") == "--json"
816
+ report = workspace.workload(limit:)
817
+ File.write(save, Mxrb::Oql::WorkloadBaseline.dump(report)) if save
818
+ comparison = Mxrb::Oql::WorkloadBaseline.compare(report, compare) if compare
819
+ render_workload_report(report, json:)
820
+ unless json
821
+ comparison&.deltas&.each do |delta|
822
+ puts "[DELTA] #{delta.query_id} #{delta.metric}: #{delta.change_percent}%"
823
+ end
824
+ end
825
+ when "indexes"
826
+ abort 'db indexes currently requires PostgreSQL' unless engine == :postgresql
827
+
828
+ limit = option.call('--limit', 50)
829
+ json = ARGV.delete('--json') == '--json'
830
+ render_index_advice(workspace.index_advice(limit:), json:)
831
+ when "shell"
832
+ write = ARGV.delete("--write")
833
+ exec(*workspace.shell_command(write:))
834
+ else
835
+ abort "Unknown db action #{action.inspect}; " \
836
+ "use up, sync, down, destroy, status, url, sql, explain, workload, indexes or shell"
837
+ end
838
+
839
+ when "serve"
840
+ path = ARGV.shift or abort "Usage: mxrb serve <file.mpr> [--port PORT] [--db-port PORT] [--no-up]"
841
+ option = lambda do |name, fallback|
842
+ index = ARGV.index(name)
843
+ next fallback unless index
844
+
845
+ abort "#{name} requires a value" unless ARGV[index + 1]
846
+ ARGV.slice!(index, 2).last
847
+ end
848
+ port = option.call("--port", 4567)
849
+ database_port = option.call("--db-port", 55_432)
850
+ prepare = !ARGV.delete("--no-up")
851
+ server = Mxrb::Oql::Server.new(path, port:, database_port:)
852
+ puts "[mxrb] Read-only query server: http://127.0.0.1:#{port}/query"
853
+ puts "[mxrb] Accepts POST JSON with an sql or oql field"
854
+ trap("INT") { Thread.new { server.shutdown } }
855
+ server.start(prepare:)
856
+
857
+ when "search"
858
+ query = ARGV.shift or
859
+ abort "Usage: mxrb search <text> <file.mpr> [--backend auto|tfidf|onnx] [--limit N]"
860
+ path = ARGV.shift or
861
+ abort "Usage: mxrb search <text> <file.mpr> [--backend auto|tfidf|onnx] [--limit N]"
862
+ option = lambda do |name, fallback|
863
+ index = ARGV.index(name)
864
+ next fallback unless index
865
+
866
+ abort "#{name} requires a value" unless ARGV[index + 1]
867
+ ARGV.slice!(index, 2).last
868
+ end
869
+ backend = option.call("--backend", "auto").to_sym
870
+ limit = option.call("--limit", 10)
871
+ json = ARGV.delete("--json")
872
+ begin
873
+ hits = Mxrb.open(path) { _1.semantic_search_hits(query, backend:, limit:) }
874
+ rescue ArgumentError, LoadError => e
875
+ abort "[mxrb] error: #{e.message}"
876
+ end
877
+ if json
878
+ puts JSON.pretty_generate(
879
+ hits.each_with_index.map do |hit, index|
880
+ {
881
+ rank: index + 1, qualified_name: hit.artifact.qualified_name,
882
+ kind: hit.artifact.kind, distance: hit.distance
883
+ }
884
+ end
885
+ )
886
+ else
887
+ puts "rank\tdistance\tqualified_name\tkind"
888
+ hits.each_with_index do |hit, index|
889
+ puts [
890
+ index + 1, format("%.6f", hit.distance), hit.artifact.qualified_name, hit.artifact.kind
891
+ ].join("\t")
892
+ end
893
+ end
894
+
895
+ when "find"
896
+ path = ARGV.shift or abort "Usage: mxrb find <file.mpr> <text> [--semantic]"
897
+ query = ARGV.shift or abort "Usage: mxrb find <file.mpr> <text> [--semantic]"
898
+ semantic = ARGV.delete("--semantic")
899
+ Mxrb.open(path) do |project|
900
+ results = if semantic
901
+ project.semantic_search_artifacts(query)
902
+ else
903
+ project.search_artifacts(query)
904
+ end
905
+ results.each do |artifact|
906
+ puts "#{artifact.qualified_name}\t#{artifact.kind}"
907
+ end
908
+ end
909
+
910
+ when "evaluate"
911
+ path = ARGV.shift or abort "Usage: mxrb evaluate <file.mpr> <evaluation.rb>"
912
+ definition = ARGV.shift or abort "Usage: mxrb evaluate <file.mpr> <evaluation.rb>"
913
+ abort "#{definition}: file not found" unless File.file?(definition)
914
+ result = Mxrb.open(path) do |project|
915
+ Mxrb::Evaluation::Suite.new(project).evaluate(definition).run
916
+ end
917
+ result.checks.each do |check|
918
+ status = check.passed? ? "pass" : check.severity
919
+ puts "[#{status}] #{check.name}: #{check.message}"
920
+ end
921
+ puts "[mxrb] score #{format('%.2f', result.score)}% " \
922
+ "(#{result.checks.count(&:passed?)}/#{result.checks.size})"
923
+ exit 1 unless result.passed?
924
+
925
+ when "test"
926
+ path = ARGV.shift or abort "Usage: mxrb test <file.mpr> <functional_test.rb> [--plan]"
927
+ definition_path = ARGV.shift or abort "Usage: mxrb test <file.mpr> <functional_test.rb> [--plan]"
928
+ abort "#{definition_path}: file not found" unless File.file?(definition_path)
929
+ plan_only = ARGV.delete("--plan")
930
+ docker = ARGV.delete("--docker")
931
+ native = ARGV.delete("--native")
932
+ abort "--native and --docker are mutually exclusive" if native && docker
933
+ report_option = lambda do |name|
934
+ index = ARGV.index(name)
935
+ next unless index
936
+
937
+ abort "#{name} requires a path" unless ARGV[index + 1]
938
+ ARGV.slice!(index, 2).last
939
+ end
940
+ json_report = report_option.call("--json")
941
+ junit_report = report_option.call("--junit")
942
+
943
+ definition = Mxrb.functional_definition(definition_path)
944
+ plan = Mxrb.runtime_plan(path)
945
+ if plan_only
946
+ puts "Mendix : #{plan.mendix_version}"
947
+ puts "Java : #{plan.java_version}"
948
+ puts "JDK builder : #{plan.jdk_package}"
949
+ puts "JRE runtime : #{plan.jre_package}"
950
+ puts "Toolchain : #{plan.toolchain_path} (#{plan.available? ? 'available' : 'missing'})"
951
+ puts "Tests : #{definition.tests.size}"
952
+ definition.tests.each { puts " #{_1.name} -> #{_1.target}" }
953
+ puts "Build : MXRB native host compiler (no mxbuild)"
954
+ else
955
+ executor = if native
956
+ Mxrb::Runtime::Native::Executor
957
+ elsif docker
958
+ Mxrb::Runtime::DockerExecutor
959
+ else
960
+ Mxrb::Runtime::Executor
961
+ end
962
+ execution = executor.new(
963
+ path, definition, plan: plan, output: $stdout
964
+ ).run
965
+ execution.result.tests.each do |test|
966
+ puts "[#{test.passed? ? 'pass' : 'fail'}] #{test.name}: #{test.message}"
967
+ end
968
+ puts "[mxrb] #{execution.result.tests.count(&:passed?)}/" \
969
+ "#{execution.result.tests.size} passed in #{format('%.2f', execution.elapsed)}s"
970
+ reporter = Mxrb::Functional::Reporter.new
971
+ reporter.write_json(execution, json_report) if json_report
972
+ reporter.write_junit(execution, junit_report) if junit_report
973
+ exit 1 unless execution.passed?
974
+ end
975
+
976
+ when "functional-instrument"
977
+ path = ARGV.shift or abort "Usage: mxrb functional-instrument <writable.mpr> <functional_test.rb>"
978
+ definition_path = ARGV.shift or
979
+ abort "Usage: mxrb functional-instrument <writable.mpr> <functional_test.rb>"
980
+ definition = Mxrb.functional_definition(definition_path)
981
+ instrumenter = Mxrb::Functional::Instrumenter.new(path, definition).instrument!
982
+ puts "[mxrb] Instrumented #{definition.tests.size} tests; runner #{instrumenter.runner}"
983
+
984
+ when "team-server"
985
+ usage = <<~USAGE
986
+ Usage: mxrb team-server <login|projects|info|branches|commits|clone|status|fetch|pull|push> ...
987
+
988
+ team-server login --pat-file FILE
989
+ Store only FILE's absolute path; the PAT is never copied into MXRB.
990
+ team-server projects [--pat-file FILE]
991
+ List every company-owned project through the official Projects API.
992
+ team-server info APP_ID [--pat-file FILE]
993
+ team-server branches APP_ID [--pat-file FILE]
994
+ team-server commits APP_ID BRANCH [--pat-file FILE]
995
+ Query the official App Repository API.
996
+ team-server clone APP_ID|URL TARGET [--branch NAME] [--depth N] [--pat-file FILE]
997
+ Clone through Git HTTPS and validate every root MPR with MXRB.
998
+ team-server status|fetch|pull PATH [--pat-file FILE]
999
+ team-server push PATH [--remote NAME] [--branch NAME] [--pat-file FILE]
1000
+
1001
+ Authentication:
1002
+ Recommended: --pat-file FILE or MXRB_TEAM_SERVER_PAT_FILE=/absolute/path/.env
1003
+ FILE may be plain text, JSON, or .env with MXRB_TEAM_SERVER_PAT=...
1004
+ Without a PAT file Git operations use the user's configured Git credential helper.
1005
+ Required scopes: mx:modelrepository:repo:read (read) and
1006
+ mx:modelrepository:repo:write (push).
1007
+
1008
+ PAT values are passed via a temporary GIT_ASKPASS helper. They are never
1009
+ embedded in repository URLs, command arguments, or .git/config.
1010
+ USAGE
1011
+ action = ARGV.shift
1012
+ if action.nil? || %w[-h --help help].include?(action)
1013
+ puts usage
1014
+ exit(action.nil? ? 1 : 0)
1015
+ end
1016
+ option = lambda do |name|
1017
+ index = ARGV.index(name)
1018
+ next unless index
1019
+
1020
+ abort "#{name} requires a value" unless ARGV[index + 1]
1021
+ ARGV.slice!(index, 2).last
1022
+ end
1023
+ pat_file = option.call("--pat-file")
1024
+ credentials = Mxrb::TeamServer::Credentials.new(pat_file:)
1025
+ repository = Mxrb::TeamServer::Repository.new(credentials:)
1026
+ api = -> { Mxrb::TeamServer::Api.new(credentials:) }
1027
+ begin
1028
+ case action
1029
+ when "login"
1030
+ abort "Usage: mxrb team-server login --pat-file FILE" unless pat_file && ARGV.empty?
1031
+ path = credentials.configure_pat_file(pat_file)
1032
+ puts "[mxrb] Stored Team Server PAT file reference in #{path}"
1033
+ puts "[mxrb] The PAT remains in #{File.expand_path(pat_file)} and is read only for requests."
1034
+ when "info"
1035
+ app_id = ARGV.shift or abort "Usage: mxrb team-server info APP_ID [--pat-file FILE]"
1036
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
1037
+ puts JSON.pretty_generate(api.call.info(app_id))
1038
+ when "projects"
1039
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
1040
+ puts JSON.pretty_generate(Mxrb::TeamServer::ProjectsApi.new(credentials:).all)
1041
+ when "branches"
1042
+ app_id = ARGV.shift or abort "Usage: mxrb team-server branches APP_ID [--pat-file FILE]"
1043
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
1044
+ puts JSON.pretty_generate(api.call.branches(app_id))
1045
+ when "commits"
1046
+ app_id = ARGV.shift or abort "Usage: mxrb team-server commits APP_ID BRANCH [--pat-file FILE]"
1047
+ branch = ARGV.shift or abort "Usage: mxrb team-server commits APP_ID BRANCH [--pat-file FILE]"
1048
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
1049
+ puts JSON.pretty_generate(api.call.commits(app_id, branch))
1050
+ when "clone"
1051
+ source = ARGV.shift or abort "Usage: mxrb team-server clone APP_ID|URL TARGET [options]"
1052
+ target = ARGV.shift or abort "Usage: mxrb team-server clone APP_ID|URL TARGET [options]"
1053
+ branch = option.call("--branch")
1054
+ depth = option.call("--depth")
1055
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
1056
+ result = repository.clone(source, target:, branch:, depth:)
1057
+ puts "[mxrb] Cloned #{result.repository_url} to #{result.root}"
1058
+ result.mpr_files.each { puts "[mxrb] Valid MPR: #{_1}" }
1059
+ when "status", "fetch", "pull"
1060
+ root = ARGV.shift || Dir.pwd
1061
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
1062
+ result = repository.public_send(action, root)
1063
+ puts(result.respond_to?(:output) ? result.output : result)
1064
+ when "push"
1065
+ root = ARGV.shift || Dir.pwd
1066
+ remote = option.call("--remote") || "origin"
1067
+ branch = option.call("--branch")
1068
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
1069
+ result = repository.push(root, remote:, branch:)
1070
+ puts result.output
1071
+ else
1072
+ abort "Unknown team-server action #{action.inspect}"
1073
+ end
1074
+ rescue Mxrb::TeamServerError => e
1075
+ warn "[mxrb] Team Server: #{e.message}"
1076
+ if e.message.match?(/Username|auth/i)
1077
+ warn "[mxrb] Configure a PAT file with: mxrb team-server login --pat-file FILE"
1078
+ end
1079
+ exit 1
1080
+ end
1081
+
1082
+ when "marketplace"
1083
+ marketplace_usage = <<~USAGE
1084
+ Usage: mxrb marketplace <login|search|show|versions|pull|dependencies|update|remove|import|audit|list|verify> ...
1085
+
1086
+ marketplace login --pat-file FILE [--no-verify]
1087
+ Recommended: store only a path and read the PAT from FILE or a Ruby .env.
1088
+ marketplace login --store-pat [--no-verify]
1089
+ Explicit opt-in: store the PAT in MXRB's own credentials file.
1090
+ marketplace search NAME [--private|--public] [--approved] [--published-since DATE]
1091
+ Search public and company-private content through the official Content API.
1092
+ marketplace show <NAME|CONTENT_ID>
1093
+ Show publisher, type, support, license, privacy, approval, and latest version.
1094
+ marketplace versions <NAME|CONTENT_ID> [--mendix-version VERSION]
1095
+ List releases, compatibility, notes, and vulnerability/security-fix metadata.
1096
+ marketplace import FILE.mpk --mpr FILE.mpr
1097
+ Import an official Mendix module package directly with Ruby/SQLite/BSON.
1098
+ marketplace pull NAME[@VERSION] --mpr FILE.mpr [--mendix-version VERSION]
1099
+ Download official Module, Service, or Widget content and install it transactionally.
1100
+ marketplace pull github:ORG/REPO --mpr FILE.mpr
1101
+ Use a public GitHub release instead of the official Marketplace.
1102
+ marketplace update NAME[@VERSION] [--mpr FILE.mpr] [--apply]
1103
+ Preview a reference-safe official update; mutate only with --apply.
1104
+ marketplace remove NAME [--mpr FILE.mpr] [--apply]
1105
+ Preview reference-safe removal; mutate only with --apply.
1106
+ marketplace dependencies NAME [--mpr FILE.mpr] [--apply|--apply-resolved]
1107
+ Resolve module references and WidgetIds; verify every downloaded MPK identity.
1108
+ marketplace audit [--target DIR] [--mendix-version VERSION]
1109
+ Check locked official components for updates and known vulnerabilities.
1110
+ marketplace list|verify [--target DIR]
1111
+ Inspect or verify .mxrb/marketplace.lock.json and cached packages.
1112
+
1113
+ Module/widget installation does not invoke mx, mxcli, Studio Pro, or the Model SDK.
1114
+ USAGE
1115
+ marketplace_login_usage = <<~USAGE
1116
+ Usage:
1117
+ mxrb marketplace login --pat-file FILE [--no-verify] # recommended
1118
+ mxrb marketplace login --store-pat [--no-verify] # explicit opt-in
1119
+
1120
+ The recommended mode stores only FILE's absolute path in
1121
+ $XDG_CONFIG_HOME/mxrb/credentials (default: ~/.config/mxrb/credentials).
1122
+ MXRB does not copy, move, chmod, or rewrite FILE. It reads the PAT only when
1123
+ an official Marketplace operation needs authentication.
1124
+
1125
+ FILE may be plain text, JSON with {"mendix_pat":"..."}, or a Ruby .env with:
1126
+ MXRB_MENDIX_PAT=your_pat
1127
+
1128
+ --store-pat stores {"mendix_pat":"..."} directly in MXRB's credentials
1129
+ file with mode 0600. The command shows the destination before prompting.
1130
+
1131
+ Environment-only alternative (nothing is persisted by login):
1132
+ MXRB_MENDIX_PAT_FILE=/absolute/path/.env mxrb marketplace search NAME
1133
+
1134
+ A direct MXRB_MENDIX_PAT value remains supported and takes precedence, but
1135
+ can be exposed through shell history/process environments. Required scope:
1136
+ mx:marketplace-content:read.
1137
+ USAGE
1138
+ action = ARGV.shift
1139
+ if action.nil? || %w[-h --help help].include?(action)
1140
+ puts marketplace_usage
1141
+ exit(action.nil? ? 1 : 0)
1142
+ end
1143
+ if action == 'login' && ARGV.any? { %w[-h --help help].include?(_1) }
1144
+ puts marketplace_login_usage
1145
+ exit 0
1146
+ end
1147
+ option = lambda do |name|
1148
+ index = ARGV.index(name)
1149
+ next unless index
1150
+
1151
+ abort "#{name} requires a value" unless ARGV[index + 1]
1152
+ ARGV.slice!(index, 2).last
1153
+ end
1154
+ mpr = option.call('--mpr')
1155
+ target = option.call('--target') || (mpr ? File.dirname(File.expand_path(mpr)) : Dir.pwd)
1156
+ plan_state = lambda do |plan|
1157
+ next 'partial applied' if plan.applied? && !plan.safe?
1158
+ next 'applied' if plan.applied?
1159
+
1160
+ plan.safe? ? 'safe preview' : 'blocked preview'
1161
+ end
1162
+ print_lifecycle_plan = lambda do |plan|
1163
+ state = plan_state.call(plan)
1164
+ version = plan.target_version ? " -> #{plan.target_version}" : ''
1165
+ puts "[mxrb] #{plan.action} #{plan.name} #{plan.installed_version}#{version}: #{state}"
1166
+ plan.changes.each { puts " change: #{_1}" }
1167
+ plan.blockers.each { puts " blocker: #{_1}" }
1168
+ end
1169
+ begin
1170
+ case action
1171
+ when 'login'
1172
+ pat_file = option.call('--pat-file')
1173
+ store_pat = ARGV.delete('--store-pat')
1174
+ no_verify = ARGV.delete('--no-verify')
1175
+ abort '--pat-file and --store-pat are mutually exclusive' if pat_file && store_pat
1176
+ unless ARGV.empty?
1177
+ abort "Unknown marketplace login option(s): #{ARGV.join(' ')}\n\n#{marketplace_login_usage}"
1178
+ end
1179
+ unless pat_file || store_pat
1180
+ warn marketplace_login_usage
1181
+ exit 1
1182
+ end
1183
+
1184
+ credentials = Mxrb::OfficialMarketplace::Credentials.new
1185
+ if pat_file
1186
+ secret_path = File.expand_path(pat_file)
1187
+ path = credentials.configure_pat_file(secret_path)
1188
+ Mxrb::OfficialMarketplace::ContentApi.new(pat: credentials.pat).search(limit: 1) unless no_verify
1189
+ puts "[mxrb] PAT file reference saved to #{path}"
1190
+ puts "[mxrb] Secret remains in #{secret_path}; MXRB reads it only for Marketplace requests."
1191
+ else
1192
+ puts '[mxrb] Explicit managed-secret mode selected.'
1193
+ puts "[mxrb] Destination: #{credentials.path}"
1194
+ puts '[mxrb] Format: JSON field "mendix_pat"; file mode: 0600.'
1195
+ puts '[mxrb] Recommended alternative: marketplace login --pat-file PATH'
1196
+ print 'Mendix PAT: '
1197
+ token = ENV['MXRB_MENDIX_PAT'] || $stdin.noecho(&:gets)
1198
+ puts
1199
+ Mxrb::OfficialMarketplace::ContentApi.new(pat: token).search(limit: 1) unless no_verify
1200
+ path = credentials.save_pat(token)
1201
+ puts "[mxrb] PAT stored by MXRB at #{path}"
1202
+ end
1203
+ when 'search'
1204
+ query = ARGV.shift or abort 'Usage: mxrb marketplace search <query>'
1205
+ source = option.call('--source') || 'official'
1206
+ if source == 'github'
1207
+ results = Mxrb::OfficialMarketplace::GitHubResolver.new.search(query)
1208
+ results.each { puts "#{_1[:name]}\t#{_1[:repository]}\t#{_1[:description]}" }
1209
+ else
1210
+ abort "Unknown Marketplace source #{source.inspect}" unless source == 'official'
1211
+
1212
+ private_flag = ARGV.delete('--private')
1213
+ public_flag = ARGV.delete('--public')
1214
+ abort '--private and --public are mutually exclusive' if private_flag && public_flag
1215
+ private_filter = true if private_flag
1216
+ private_filter = false if public_flag
1217
+ approved = true if ARGV.delete('--approved')
1218
+ results = Mxrb::OfficialMarketplace::ContentApi.new.search(
1219
+ name: query, private: private_filter, approved:,
1220
+ published_since: option.call('--published-since'), limit: option.call('--limit') || 10
1221
+ )
1222
+ results.each do |item|
1223
+ latest = item.fetch('latestVersion', {})
1224
+ visibility = item['isPrivate'] ? 'private' : 'public' # rubocop:disable Metrics/BlockNesting
1225
+ puts [item['contentId'], latest['name'], latest['versionNumber'], item['type'],
1226
+ item['publisher'], visibility].join("\t")
1227
+ end
1228
+ puts "[mxrb] #{results.size} official Marketplace result(s)"
1229
+ end
1230
+ when 'show'
1231
+ identifier = ARGV.shift or abort 'Usage: mxrb marketplace show <name|content_id>'
1232
+ puts JSON.pretty_generate(Mxrb::OfficialMarketplace::ContentApi.new.find(identifier))
1233
+ when 'versions'
1234
+ identifier = ARGV.shift or abort 'Usage: mxrb marketplace versions <name|content_id>'
1235
+ api = Mxrb::OfficialMarketplace::ContentApi.new
1236
+ content = api.find(identifier)
1237
+ mendix_version = option.call('--mendix-version')
1238
+ published_since = option.call('--published-since')
1239
+ versions = if mendix_version
1240
+ api.versions(content.fetch('contentId'), mendix_version:, published_since:)
1241
+ else
1242
+ api.all_versions(content.fetch('contentId'), published_since:)
1243
+ end
1244
+ versions.each do |entry|
1245
+ issues = Array(entry['vulnerabilities']) + Array(entry['fixedSecurityIssues'])
1246
+ puts [entry['versionId'], entry['versionNumber'], entry['minSupportedMendixVersion'],
1247
+ entry['versionType'], issues.filter_map { _1['code'] }.join(','),
1248
+ entry['publicationDate']].join("\t")
1249
+ puts " #{entry['releaseNotes']}" unless entry['releaseNotes'].to_s.empty?
1250
+ end
1251
+ puts "[mxrb] #{versions.size} version(s)"
1252
+ when 'pull'
1253
+ identifier = ARGV.shift or
1254
+ abort 'Usage: mxrb marketplace pull <name[@version]|github:org/repo> [--mpr FILE]'
1255
+ source = option.call('--source') || (identifier.start_with?('github:') ? 'github' : 'official')
1256
+ name, version = identifier.split('@', 2)
1257
+ installer = Mxrb::OfficialMarketplace::Installer.new(target:, mpr:)
1258
+ allow_vulnerable = !ARGV.delete('--allow-vulnerable').nil?
1259
+ result = case source
1260
+ when 'official'
1261
+ installer.pull_official(
1262
+ name, version:, mendix_version: option.call('--mendix-version'), allow_vulnerable:
1263
+ )
1264
+ when 'github'
1265
+ installer.pull(name, version:)
1266
+ else
1267
+ abort "Unknown Marketplace source #{source.inspect}"
1268
+ end
1269
+ puts "[mxrb] Installed #{result.package.name} #{result.package.version}"
1270
+ puts "[mxrb] #{result.destination}"
1271
+ puts "[mxrb] Imported #{result.units} MPR units without Studio Pro" if result.respond_to?(:units)
1272
+ puts "[mxrb] SHA-256 #{result.sha256}"
1273
+ when 'import'
1274
+ path = ARGV.shift or
1275
+ abort 'Usage: mxrb marketplace import <file.mpk> [--mpr FILE] [--target DIR]'
1276
+ result = Mxrb::OfficialMarketplace::Installer.new(target:, mpr:).import(
1277
+ path, name: option.call('--name')
1278
+ )
1279
+ puts "[mxrb] Imported #{result.package.name} into #{result.destination}"
1280
+ puts "[mxrb] #{result.units} MPR units written without Studio Pro" if result.respond_to?(:units)
1281
+ puts "[mxrb] SHA-256 #{result.sha256}"
1282
+ when 'update'
1283
+ identifier = ARGV.shift or
1284
+ abort 'Usage: mxrb marketplace update <name[@version]> [--mpr FILE] [--apply]'
1285
+ name, version = identifier.split('@', 2)
1286
+ plan = Mxrb::OfficialMarketplace::Installer.new(target:, mpr:).update_official(
1287
+ name, version:,
1288
+ mendix_version: option.call('--mendix-version'),
1289
+ allow_vulnerable: !ARGV.delete('--allow-vulnerable').nil?,
1290
+ apply: !ARGV.delete('--apply').nil?
1291
+ )
1292
+ print_lifecycle_plan.call(plan)
1293
+ exit 1 unless plan.safe?
1294
+ when 'remove'
1295
+ identifier = ARGV.shift or
1296
+ abort 'Usage: mxrb marketplace remove <name> [--mpr FILE] [--apply]'
1297
+ plan = Mxrb::OfficialMarketplace::Installer.new(target:, mpr:).remove(
1298
+ identifier, apply: !ARGV.delete('--apply').nil?
1299
+ )
1300
+ print_lifecycle_plan.call(plan)
1301
+ exit 1 unless plan.safe?
1302
+ when 'dependencies'
1303
+ identifier = ARGV.shift or
1304
+ abort 'Usage: mxrb marketplace dependencies <name> [--mpr FILE] [--apply|--apply-resolved]'
1305
+ apply = !ARGV.delete('--apply').nil?
1306
+ apply_resolved = !ARGV.delete('--apply-resolved').nil?
1307
+ abort '--apply and --apply-resolved are mutually exclusive' if apply && apply_resolved
1308
+ plan = Mxrb::OfficialMarketplace::Installer.new(target:, mpr:).dependencies(
1309
+ identifier, apply:, apply_resolved:
1310
+ )
1311
+ state = plan_state.call(plan)
1312
+ puts "[mxrb] dependencies #{plan.root}: #{state}"
1313
+ plan.changes.each { puts " change: #{_1}" }
1314
+ plan.blockers.each { puts " blocker: #{_1}" }
1315
+ exit 1 unless plan.safe?
1316
+ when 'list'
1317
+ packages = Mxrb::OfficialMarketplace.lock(target).fetch('packages')
1318
+ packages.each { |name, entry| puts "#{name}\t#{entry['version']}\t#{entry['source']}" }
1319
+ puts "[mxrb] #{packages.size} package(s)"
1320
+ when 'audit'
1321
+ results = Mxrb::OfficialMarketplace::SecurityAuditor.new(target:).audit(
1322
+ mendix_version: option.call('--mendix-version')
1323
+ )
1324
+ results.each do |result|
1325
+ state = result.version_type == 'Unknown' ? 'unknown' : 'vulnerable'
1326
+ state = result.outdated ? 'update' : 'ok' if result.valid
1327
+ puts [state, result.name, result.installed_version, result.latest_version,
1328
+ result.version_type, result.issues.join(',')].join("\t")
1329
+ end
1330
+ puts "[mxrb] #{results.size} official Marketplace package(s) audited"
1331
+ exit 1 unless results.all?(&:valid)
1332
+ when 'verify'
1333
+ results = Mxrb::OfficialMarketplace.verify(target)
1334
+ results.each { |name, result| puts "#{result[:valid] ? 'ok' : 'changed'}\t#{name}" }
1335
+ exit 1 unless results.values.all? { _1[:valid] }
1336
+ else
1337
+ abort "Unknown marketplace action #{action.inspect}"
1338
+ end
1339
+ rescue Mxrb::MarketplaceError => e
1340
+ abort "[mxrb] error: #{e.message}"
1341
+ end
1342
+
1343
+ when "module"
1344
+ action = ARGV.shift or abort "Usage: mxrb module <new|search|add> ..."
1345
+ option = lambda do |name|
1346
+ index = ARGV.index(name)
1347
+ next unless index
1348
+
1349
+ abort "#{name} requires a value" unless ARGV[index + 1]
1350
+ ARGV.slice!(index, 2).last
1351
+ end
1352
+ case action
1353
+ when "new"
1354
+ name = ARGV.shift or abort "Usage: mxrb module new <name> [--target DIR]"
1355
+ target = option.call("--target") || Dir.pwd
1356
+ begin
1357
+ result = Mxrb::ModuleInitializer.new(name).scaffold(into: target)
1358
+ rescue ArgumentError => e
1359
+ abort "[mxrb] error: #{e.message}"
1360
+ end
1361
+ result.files.each { puts " create #{_1}" }
1362
+ puts " update #{result.project_file}"
1363
+ puts "\nDone. Run:"
1364
+ puts " bundle exec mxrb generate project.rb"
1365
+ when "search"
1366
+ registry = option.call("--registry")
1367
+ catalog = Mxrb::Marketplace::Catalog.new(registry)
1368
+ query = ARGV.shift
1369
+ entries = catalog.search(query)
1370
+ entries.each do |entry|
1371
+ puts "#{entry.name}\t#{entry.version}\t#{entry.description}"
1372
+ end
1373
+ puts "[mxrb] #{entries.size} module(s)"
1374
+ when "add"
1375
+ registry = option.call("--registry")
1376
+ catalog = Mxrb::Marketplace::Catalog.new(registry)
1377
+ identifier = ARGV.shift or
1378
+ abort "Usage: mxrb module add <name|directory> [--target DIR] [--registry SOURCE]"
1379
+ target = option.call("--target") || Dir.pwd
1380
+ installation = Mxrb::Marketplace::Installer.new(
1381
+ target: target, catalog: catalog
1382
+ ).install(identifier)
1383
+ puts "[mxrb] Installed #{installation.entry.name} " \
1384
+ "#{installation.entry.version} as #{installation.module_name}"
1385
+ puts "[mxrb] #{installation.destination}"
1386
+ puts "[mxrb] SHA-256 #{installation.digest}"
1387
+ else
1388
+ abort "Unknown module action #{action.inspect}; use new, search, or add"
1389
+ end
1390
+
1391
+ when 'scaffold'
1392
+ action = ARGV.shift or abort 'Usage: mxrb scaffold <list|destroy> ...'
1393
+ target_index = ARGV.index('--target')
1394
+ target = if target_index
1395
+ abort '--target requires a value' unless ARGV[target_index + 1]
1396
+ ARGV.slice!(target_index, 2).last
1397
+ else
1398
+ Dir.pwd
1399
+ end
1400
+ case action
1401
+ when 'list'
1402
+ puts "command\tdestination"
1403
+ Mxrb::Scaffold::Help::COMMANDS.each do |name, (_usage, _description, destination)|
1404
+ puts "#{name}\t#{destination}"
1405
+ end
1406
+ registry = Mxrb::Scaffold::Registry.new(target).entries
1407
+ unless registry.empty?
1408
+ puts "\nregistered scaffold\tfiles"
1409
+ registry.each { |key, entry| puts "#{key}\t#{entry.fetch('files').size}" }
1410
+ end
1411
+ when 'destroy'
1412
+ key = ARGV.shift or abort 'Usage: mxrb scaffold destroy <kind:name> [--target DIR]'
1413
+ removal = Mxrb::Scaffold::Registry.new(target).remove(key)
1414
+ removal.files.each { puts " remove #{_1}" }
1415
+ else
1416
+ abort "Unknown scaffold action #{action.inspect}; use list or destroy"
1417
+ end
1418
+
1419
+ when *SCAFFOLD_COMMANDS
1420
+ Mxrb::Scaffold::CLI.new(command, ARGV).run
1421
+
1422
+ when "describe"
1423
+ path = ARGV.shift or abort "Usage: mxrb describe <file.mpr> <qualified-name>"
1424
+ name = ARGV.shift or abort "Usage: mxrb describe <file.mpr> <qualified-name>"
1425
+ begin
1426
+ Mxrb.open(path) do |project|
1427
+ details = project.describe_artifact(name)
1428
+ puts "#{details.artifact.qualified_name}\t#{details.artifact.kind}"
1429
+ puts "Incoming:"
1430
+ details.incoming.each do |reference|
1431
+ puts " #{reference.source.qualified_name}\t#{reference.relation}"
1432
+ end
1433
+ puts "Outgoing:"
1434
+ details.outgoing.each do |reference|
1435
+ puts " #{reference.target.qualified_name}\t#{reference.relation}"
1436
+ end
1437
+ end
1438
+ rescue KeyError, ArgumentError => e
1439
+ warn "[mxrb] error: #{e.message}"
1440
+ exit 1
1441
+ end
1442
+
1443
+ when "tree"
1444
+ path = ARGV.shift or abort "Usage: mxrb tree <file.mpr> [module]"
1445
+ selected_module = ARGV.shift
1446
+ Mxrb.open(path) do |project|
1447
+ artifacts = project.semantic_index.artifacts
1448
+ artifacts = artifacts.select { _1.module_name == selected_module } if selected_module
1449
+ artifacts.group_by(&:module_name).sort_by { |module_name, _| module_name.to_s }
1450
+ .each do |module_name, module_artifacts|
1451
+ puts module_name || "(project)"
1452
+ module_artifacts.reject { _1.kind == :module }.group_by(&:kind)
1453
+ .sort_by { |kind, _| kind.to_s }.each do |kind, items|
1454
+ puts " #{kind}"
1455
+ items.sort_by(&:qualified_name).each { puts " #{_1.qualified_name}" }
1456
+ end
1457
+ end
1458
+ end
1459
+
1460
+ when "init"
1461
+ name = ARGV.shift or abort "Usage: mxrb init <project_name> [--mxrb-path DIR]"
1462
+ path_index = ARGV.index('--mxrb-path')
1463
+ mxrb_path = path_index ? ARGV.slice!(path_index, 2).last : nil
1464
+ abort '--mxrb-path requires a value' if path_index && mxrb_path.nil?
1465
+ abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
1466
+ begin
1467
+ result = Mxrb::Initializer.new(name, mxrb_path:).scaffold
1468
+ rescue ArgumentError => e
1469
+ abort "[mxrb] error: #{e.message}"
1470
+ end
1471
+ result.files.each { puts " create #{_1}" }
1472
+ puts "\nDone. Next steps:"
1473
+ puts " cd #{result.root}"
1474
+ puts " bundle install"
1475
+ puts " bundle exec mxrb generate project.rb"
1476
+
1477
+ else
1478
+ puts <<~USAGE
1479
+ mxrb — Pure-Ruby Mendix .mpr toolkit
1480
+
1481
+ Commands:
1482
+ doctor [DIR] [--json] Check project files and local toolchain
1483
+ benchmark <file.mpr> Measure open, index and validation time
1484
+ --iterations N --json Configure repetitions or emit JSON
1485
+ project inspect [DIR] [--json] Inventory version, modules, MPRs and scaffolds
1486
+ upgrade --mendix VERSION Preview a project.rb version upgrade
1487
+ [--target DIR] --apply Apply the version change transactionally
1488
+ migrate check|plan [DIR] [--json] Compare project.rb output with the current MPR
1489
+ inspect <file.mpr> Full unit dump (exploration)
1490
+ dump-unit <file.mpr> <unit_id> Hex dump of a unit's Contents blob
1491
+ sql <file.mpr> "SELECT …" Run raw SQL against the .mpr SQLite
1492
+ modules <file.mpr> List all modules
1493
+ init <project_name> Scaffold a new Ruby-first Mendix project
1494
+ --mxrb-path DIR Use a local MXRB checkout in the Gemfile
1495
+ generate <definition.rb> [mpr] Apply a Ruby DSL definition
1496
+ pack <file.mpr> Build an MDA from a materialized deployment
1497
+ --output FILE [--force] Pure Ruby container writer; never calls mx/mxbuild
1498
+ portable <file.mpr> Build an executable portable Runtime ZIP
1499
+ --output FILE Uses installed Runtime files; never calls mx/mxbuild
1500
+ mda inspect|compare <file.mda> Inspect or diff MDA contents and metadata
1501
+ widgets sync <definition.rb> <mpr> Synchronize pluggable widget schemas
1502
+ frontend migrate <file.mpr> Preview frontend schema/layout/theme migration
1503
+ [--apply] [--json] Apply only a safe plan or emit structured output
1504
+ export <file.mpr> <directory> Convert an MPR to editable Ruby
1505
+ validate <file.mpr> Check MPR structure, hashes and v2 contents
1506
+ preflight <file.mpr> [--json] Audit native compiler/runtime compatibility
1507
+ compare <left.mpr> <right.mpr> Compare structural MPR snapshots
1508
+ diff <left.mpr> <right.mpr> Print typed semantic changes
1509
+ refs <file.mpr> <name> Find references to a Mendix artifact
1510
+ callers <file.mpr> <name> List callers of a callable artifact
1511
+ callees <file.mpr> <name> List artifacts called by a callable artifact
1512
+ impact <file.mpr> <name> Find transitive dependents
1513
+ rename <mpr> <old> <new> Preview a safe model-wide rename
1514
+ --apply Apply the previewed rename
1515
+ remove <mpr> <name> Preview a reference-safe unit removal
1516
+ --apply Apply only when no refs/children exist
1517
+ move <mpr> <name> <container> Preview a same-module unit move
1518
+ --apply Move into a module or folder
1519
+ lint <file.mpr> Ruby semantic diagnostics
1520
+ report <file.mpr> Coupling and reference summary
1521
+ cache status <file.mpr> [--json] Inspect semantic cache health and size
1522
+ warm <file.mpr> [--json] Build or refresh the semantic cache
1523
+ clear <file.mpr> [--json] Remove semantic cache entries
1524
+ oql <file.mpr> [--json] Show native OQL and a read-only SQL view
1525
+ --dialect DIALECT ansi, postgresql (default), or sql_server
1526
+ analyze <file.mpr> [--json] Analyze every native OQL query
1527
+ --sql "SELECT ..." Analyze ad-hoc SQL instead of an MPR
1528
+ --oql "SELECT ..." Analyze ad-hoc OQL instead of an MPR
1529
+ --dialect DIALECT Dialect-specific actionable suggestions
1530
+ design scan <file.mpr> [--json] Inventory CSS and SCSS design tokens
1531
+ migrate <mpr> <literal> <token> Preview a literal-to-token migration
1532
+ --apply Apply the asset migration atomically
1533
+ db up <file.mpr> [--port PORT] [--runtime-port PORT]
1534
+ Build, synchronize and expose PostgreSQL and Runtime
1535
+ sync <file.mpr> [--port PORT] [--runtime-port PORT]
1536
+ Rebuild and synchronize the retained database
1537
+ down <file.mpr> Stop containers while preserving all data
1538
+ destroy <file.mpr> --yes Remove isolated containers, data, and state
1539
+ status <file.mpr> Show isolated database/runtime status
1540
+ url <file.mpr> [--port PORT] Print the read-only PostgreSQL connection URL
1541
+ sql <file.mpr> "SELECT …" Execute SQL as the read-only analyst role
1542
+ --write Explicitly use the Runtime owner role
1543
+ explain <file.mpr> "SELECT …" Inspect PostgreSQL plan and index findings
1544
+ --analyze Execute the read-only query with buffers/timing
1545
+ --json Emit a machine-readable plan report
1546
+ --engine sql_server Use an explicit SQL Server deployment
1547
+ --server HOST --database DB --user USER
1548
+ Read password from MXRB_SQLSERVER_PASSWORD
1549
+ --sqlcmd PATH Override the sqlcmd executable
1550
+ workload <file.mpr> [--limit N] Rank pg_stat_statements and schema pressure
1551
+ --json Emit queries, table/index stats, and findings
1552
+ --save FILE Save a workload performance baseline
1553
+ --compare FILE Report regressions against a saved baseline
1554
+ indexes <file.mpr> [--limit N] Advise evidence-backed PostgreSQL indexes
1555
+ --json Emit candidates and overlaps as JSON
1556
+ shell <file.mpr> [--write] Open psql (read-only by default)
1557
+ serve <file.mpr> [--port 4567] Serve read-only SQL/OQL over loopback HTTP
1558
+ --db-port PORT Runtime PostgreSQL host port (default 55432)
1559
+ --no-up Reuse an already-running database workspace
1560
+ search <text> <file.mpr> Semantic search with ranked distances
1561
+ --backend NAME auto, tfidf, or onnx
1562
+ --limit N Maximum results (default 10)
1563
+ find <file.mpr> <text> Search artifacts by name
1564
+ describe <file.mpr> <name> Show incoming and outgoing references
1565
+ tree <file.mpr> [module] Browse the semantic structure
1566
+ evaluate <file.mpr> <eval.rb> Run Ruby model evaluations
1567
+ module search [query] Search the Ruby module marketplace
1568
+ add <name|directory> Install a module and write its lockfile
1569
+ new <name> [--target DIR] Scaffold and connect an application module
1570
+ marketplace login --pat-file FILE Reference a PAT file (--help for secure modes)
1571
+ search <name> Search the official Content API
1572
+ show <name|content-id> Show official component metadata
1573
+ versions <name|id> List compatibility and security metadata
1574
+ pull <name|id|github:o/r> Download/import a release [--mpr FILE]
1575
+ import <file.mpk> Import a module directly [--mpr FILE]
1576
+ audit Check locked updates and vulnerabilities
1577
+ list|verify Inspect or verify locked official packages
1578
+ team-server clone APP_ID|URL TARGET Clone and validate a Mendix Team Server app
1579
+ info|branches|commits Query the official App Repository API
1580
+ status|fetch|pull|push Synchronize through Git without mx
1581
+ scaffold list [--target DIR] List generators and registered scaffolds
1582
+ destroy <kind:name> Remove an unchanged registered scaffold
1583
+ entity new Module.Name Scaffold a domain entity (--help for details)
1584
+ enumeration new Module.Name Scaffold an enumeration
1585
+ use-case new Module.Name Scaffold an application use case
1586
+ validation new Module.Name Scaffold a validation flow
1587
+ query new Module.Name Scaffold a query flow
1588
+ functional-test new Module.Flow Scaffold a runtime functional test
1589
+ evaluation new Name Scaffold static model evaluations
1590
+ presentation init Module Initialize presentation directories
1591
+ page templates [--json] List audited page patterns as a tree
1592
+ page new|generate|g Module.Name Scaffold a page
1593
+ [--template NAME] Apply starter/blank/dashboard/form-vertical
1594
+ [--chain CHAIN] Compose page/nanoflow/microflow actions
1595
+ nanoflow new Module.Name Scaffold a client nanoflow
1596
+ repository new Module.Name Scaffold a repository port and adapter
1597
+ security init Module Scaffold module roles
1598
+ scheduled-event new Module.Name Scaffold an event and handler
1599
+ constant new Module.Name Scaffold a module constant
1600
+ integration new Module.Name Scaffold an integration adapter
1601
+ published-rest new Module.Name Scaffold a published REST handler
1602
+ consumed-rest new Module.Name Scaffold a consumed REST adapter
1603
+ java-action new Module.Name Scaffold a Java Action adapter
1604
+ design init Initialize design-system Ruby
1605
+ ci init github Scaffold GitHub Actions
1606
+ test <file.mpr> <test.rb> Run isolated functional runtime tests
1607
+ --native Use the Ruby model Runtime; no Java/mxbuild
1608
+ --docker Run the natively built package in a disposable Runtime container
1609
+ --plan Show toolchain and Docker execution plan
1610
+ --json PATH Write a machine-readable result
1611
+ --junit PATH Write a JUnit-compatible result
1612
+ USAGE
1613
+ end