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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- 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
|