mendix-ruby-bridge 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.mxcli-version +2 -0
  3. data/LICENSE +21 -0
  4. data/README.md +77 -0
  5. data/bin/git-mendix +4 -0
  6. data/bin/mendix-apply +145 -0
  7. data/bin/mendix-desktop +26 -0
  8. data/bin/mendix-git +121 -0
  9. data/bin/mendix-ruby +580 -0
  10. data/bin/mxcli +20 -0
  11. data/bin/setup-tools +25 -0
  12. data/lib/mendix_bridge/backend_server.rb +1404 -0
  13. data/lib/mendix_bridge/change_planner.rb +594 -0
  14. data/lib/mendix_bridge/config.rb +89 -0
  15. data/lib/mendix_bridge/dependency_index.rb +188 -0
  16. data/lib/mendix_bridge/desktop_app.rb +121 -0
  17. data/lib/mendix_bridge/document_parser.rb +120 -0
  18. data/lib/mendix_bridge/domain_parser.rb +103 -0
  19. data/lib/mendix_bridge/dsl.rb +540 -0
  20. data/lib/mendix_bridge/enumeration_parser.rb +32 -0
  21. data/lib/mendix_bridge/git_workflow.rb +321 -0
  22. data/lib/mendix_bridge/html_viewer.rb +672 -0
  23. data/lib/mendix_bridge/importer.rb +415 -0
  24. data/lib/mendix_bridge/inventory.rb +93 -0
  25. data/lib/mendix_bridge/mdl_generator.rb +343 -0
  26. data/lib/mendix_bridge/microflow_parser.rb +131 -0
  27. data/lib/mendix_bridge/migration.rb +290 -0
  28. data/lib/mendix_bridge/migration_executor.rb +234 -0
  29. data/lib/mendix_bridge/model.rb +204 -0
  30. data/lib/mendix_bridge/page_parser.rb +95 -0
  31. data/lib/mendix_bridge/presenter.rb +35 -0
  32. data/lib/mendix_bridge/project_creator.rb +181 -0
  33. data/lib/mendix_bridge/ruby_inventory_generator.rb +63 -0
  34. data/lib/mendix_bridge/security_parser.rb +72 -0
  35. data/lib/mendix_bridge/snapshot_diff.rb +58 -0
  36. data/lib/mendix_bridge/validator.rb +46 -0
  37. data/lib/mendix_bridge/version.rb +5 -0
  38. data/lib/mendix_bridge/visual_entity_plan.rb +176 -0
  39. data/lib/mendix_bridge.rb +127 -0
  40. data/share/applications/mendix-ruby-bridge.desktop +12 -0
  41. data/share/icons/hicolor/128x128/apps/mendix-ruby-bridge.png +0 -0
  42. data/share/icons/hicolor/256x256/apps/mendix-ruby-bridge.png +0 -0
  43. data/share/icons/hicolor/32x32/apps/mendix-ruby-bridge.png +0 -0
  44. data/share/icons/hicolor/48x48/apps/mendix-ruby-bridge.png +0 -0
  45. data/share/icons/hicolor/512x512/apps/mendix-ruby-bridge.png +0 -0
  46. data/share/icons/hicolor/64x64/apps/mendix-ruby-bridge.png +0 -0
  47. data/web/dist/apple-touch-icon.png +0 -0
  48. data/web/dist/assets/index-BO6iPT1P.css +1 -0
  49. data/web/dist/assets/index-JGODw81W.js +27 -0
  50. data/web/dist/brand/mendix-ruby-bridge.png +0 -0
  51. data/web/dist/favicon.png +0 -0
  52. data/web/dist/icons.svg +24 -0
  53. data/web/dist/index.html +17 -0
  54. metadata +179 -0
data/bin/mendix-ruby ADDED
@@ -0,0 +1,580 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "optparse"
6
+ require_relative "../lib/mendix_bridge"
7
+
8
+ command = ARGV.shift
9
+ project_config = MendixBridge::Config.find
10
+
11
+ case command
12
+ when "--version", "-v", "version"
13
+ puts MendixBridge::VERSION
14
+ when "serve"
15
+ options = { bind: "127.0.0.1", port: 4567 }
16
+ parser = OptionParser.new do |opts|
17
+ opts.banner = "Usage: mendix-ruby serve [IMPORTED_DIR] [options]"
18
+ opts.on("--bind ADDRESS", "Bind address (default: 127.0.0.1)") do |address|
19
+ options[:bind] = address
20
+ end
21
+ opts.on("-p", "--port PORT", Integer, "HTTP port (default: 4567)") do |port|
22
+ options[:port] = port
23
+ end
24
+ end
25
+ parser.parse!
26
+ inventory_dir = File.expand_path(
27
+ ARGV.shift || project_config&.inventory || "."
28
+ )
29
+ unless ARGV.empty?
30
+ warn parser
31
+ exit 1
32
+ end
33
+
34
+ bridge_dir = File.expand_path("..", __dir__)
35
+ server = MendixBridge::BackendServer.new(
36
+ inventory_dir:,
37
+ web_root: File.join(bridge_dir, "web", "dist"),
38
+ mxcli: File.join(bridge_dir, "bin", "mxcli"),
39
+ bind: options.fetch(:bind),
40
+ port: options.fetch(:port)
41
+ )
42
+ %w[INT TERM].each do |signal|
43
+ Signal.trap(signal) { server.shutdown }
44
+ end
45
+ puts "Mendix Ruby Bridge: http://#{options.fetch(:bind)}:#{server.port}"
46
+ puts "Inventory: #{inventory_dir}"
47
+ server.start
48
+ when "setup"
49
+ setup = File.expand_path("setup-tools", __dir__)
50
+ abort "Tool installer is missing: #{setup}" unless File.executable?(setup)
51
+ exit(system(setup) ? 0 : 1)
52
+ when "configure"
53
+ options = { force: false, path: MendixBridge::Config::FILE_NAME }
54
+ parser = OptionParser.new do |opts|
55
+ opts.banner =
56
+ "Usage: mendix-ruby configure --project APP.mpr --inventory DIR --model APP.rb"
57
+ opts.on("-p", "--project FILE", "Mendix .mpr file") do |file|
58
+ options[:project] = File.expand_path(file)
59
+ end
60
+ opts.on("-i", "--inventory DIR", "Ruby inventory directory") do |dir|
61
+ options[:inventory] = File.expand_path(dir)
62
+ end
63
+ opts.on("-m", "--model FILE", "Ruby model file") do |file|
64
+ options[:model] = File.expand_path(file)
65
+ end
66
+ opts.on("-o", "--output FILE", "Configuration file path") do |file|
67
+ options[:path] = File.expand_path(file)
68
+ end
69
+ opts.on("--force", "Replace an existing configuration") do
70
+ options[:force] = true
71
+ end
72
+ end
73
+ parser.parse!
74
+ unless options[:project] && options[:inventory] && options[:model] && ARGV.empty?
75
+ warn parser
76
+ exit 1
77
+ end
78
+
79
+ config = MendixBridge::Config.write(
80
+ options.fetch(:path),
81
+ project: options.fetch(:project),
82
+ inventory: options.fetch(:inventory),
83
+ model: options.fetch(:model),
84
+ force: options.fetch(:force)
85
+ )
86
+ puts "Wrote #{config.path}"
87
+ puts JSON.pretty_generate(config.to_h)
88
+ when "deps"
89
+ options = { direction: "both", transitive: false, json: false }
90
+ parser = OptionParser.new do |opts|
91
+ opts.banner = "Usage: mendix-ruby deps ELEMENT [IMPORTED_DIR] [options]"
92
+ opts.on(
93
+ "-d",
94
+ "--direction DIRECTION",
95
+ %w[both dependencies dependents callers callees impact],
96
+ "both, dependencies, dependents, callers, callees, or impact"
97
+ ) { |direction| options[:direction] = direction }
98
+ opts.on("--transitive", "Traverse the complete dependency graph") do
99
+ options[:transitive] = true
100
+ end
101
+ opts.on("--json", "Print machine-readable JSON") { options[:json] = true }
102
+ end
103
+ parser.parse!
104
+ name = ARGV.shift
105
+ imported_dir = File.expand_path(
106
+ ARGV.shift || project_config&.inventory || "."
107
+ )
108
+ unless name && ARGV.empty?
109
+ warn parser
110
+ exit 1
111
+ end
112
+
113
+ snapshot_file = File.join(imported_dir, "inventory", "project-tree.json")
114
+ details_file = File.join(imported_dir, "inventory", "element-details.json")
115
+ dependencies_file = File.join(imported_dir, "inventory", "dependencies.json")
116
+ abort "Not an imported Mendix Ruby project: #{imported_dir}" unless
117
+ File.file?(snapshot_file)
118
+
119
+ inventory = MendixBridge::Inventory.load(
120
+ snapshot_file,
121
+ details_path: details_file
122
+ )
123
+ index = if File.file?(dependencies_file)
124
+ MendixBridge::DependencyIndex.load(dependencies_file, inventory:)
125
+ else
126
+ MendixBridge::DependencyIndex.new(inventory)
127
+ end
128
+ result = case options.fetch(:direction)
129
+ when "dependencies"
130
+ { "dependencies" => index.dependencies(name, transitive: options[:transitive]) }
131
+ when "dependents"
132
+ { "dependents" => index.dependents(name, transitive: options[:transitive]) }
133
+ when "callers"
134
+ { "callers" => index.callers(name, transitive: options[:transitive]) }
135
+ when "callees"
136
+ { "callees" => index.callees(name, transitive: options[:transitive]) }
137
+ when "impact"
138
+ { "impact" => index.impact(name) }
139
+ else
140
+ {
141
+ "dependencies" => index.dependencies(name, transitive: options[:transitive]),
142
+ "dependents" => index.dependents(name, transitive: options[:transitive])
143
+ }
144
+ end
145
+ result.transform_values! { |edges| edges.map(&:to_h) }
146
+
147
+ if options[:json]
148
+ puts JSON.pretty_generate({ "element" => name }.merge(result))
149
+ else
150
+ puts "Dependencies for #{name}:"
151
+ result.each do |direction, edges|
152
+ puts "#{direction.capitalize} (#{edges.length}):"
153
+ edges.each do |edge|
154
+ puts " #{edge.fetch(:from)} -> #{edge.fetch(:to)} " \
155
+ "[#{edge.fetch(:kind)} via #{edge.fetch(:path)}]"
156
+ end
157
+ end
158
+ end
159
+ when "migrate"
160
+ options = { studio_closed: false, dry_run: false }
161
+ parser = OptionParser.new do |opts|
162
+ opts.banner =
163
+ "Usage: mendix-ruby migrate MIGRATION.rb --project APP.mpr [options]"
164
+ opts.on("-p", "--project FILE", "Target Mendix .mpr file") do |file|
165
+ options[:project] = File.expand_path(file)
166
+ end
167
+ opts.on("--confirm NAME", "Confirm the exact migration name") do |name|
168
+ options[:confirmation] = name
169
+ end
170
+ opts.on("--studio-closed", "Confirm that Studio Pro is closed") do
171
+ options[:studio_closed] = true
172
+ end
173
+ opts.on("--dry-run", "Print operations and MDL without changing the project") do
174
+ options[:dry_run] = true
175
+ end
176
+ end
177
+ parser.parse!
178
+ migration_file = ARGV.shift
179
+ options[:project] ||= project_config&.project
180
+ unless migration_file && options[:project] && ARGV.empty?
181
+ warn parser
182
+ exit 1
183
+ end
184
+
185
+ plan = MendixBridge.load_migration(File.expand_path(migration_file))
186
+ if options[:dry_run]
187
+ puts JSON.pretty_generate(plan.to_h)
188
+ mdl = MendixBridge::Migration::Generator.mdl(plan)
189
+ puts "\nMDL:\n#{mdl}" unless mdl.empty?
190
+ renames = MendixBridge::Migration::Generator.rename_operations(plan)
191
+ unless renames.empty?
192
+ puts "\nRenames:"
193
+ renames.each do |operation|
194
+ puts " #{operation.type} #{operation.name} -> #{operation.options.fetch('to')}"
195
+ end
196
+ end
197
+ exit
198
+ end
199
+
200
+ unless options[:confirmation]
201
+ abort "Destructive migration requires --confirm #{plan.name}"
202
+ end
203
+ bridge_dir = File.expand_path("..", __dir__)
204
+ executor = MendixBridge::MigrationExecutor.new(
205
+ mxcli: File.join(bridge_dir, "bin", "mxcli")
206
+ )
207
+ result = executor.apply(
208
+ plan,
209
+ project_file: options.fetch(:project),
210
+ confirmation: options.fetch(:confirmation),
211
+ studio_closed: options.fetch(:studio_closed)
212
+ )
213
+ puts "Migration #{plan.name} completed (#{result.operations} operations)."
214
+ puts "Rollback backup: #{result.backup_dir}"
215
+ when "rollback"
216
+ options = { studio_closed: false }
217
+ parser = OptionParser.new do |opts|
218
+ opts.banner =
219
+ "Usage: mendix-ruby rollback BACKUP_DIR --project APP.mpr [options]"
220
+ opts.on("-p", "--project FILE", "Target Mendix .mpr file") do |file|
221
+ options[:project] = File.expand_path(file)
222
+ end
223
+ opts.on("--confirm BACKUP_ID", "Confirm the exact backup directory name") do |name|
224
+ options[:confirmation] = name
225
+ end
226
+ opts.on("--studio-closed", "Confirm that Studio Pro is closed") do
227
+ options[:studio_closed] = true
228
+ end
229
+ end
230
+ parser.parse!
231
+ backup_dir = ARGV.shift
232
+ options[:project] ||= project_config&.project
233
+ unless backup_dir && options[:project] && options[:confirmation] && ARGV.empty?
234
+ warn parser
235
+ exit 1
236
+ end
237
+
238
+ bridge_dir = File.expand_path("..", __dir__)
239
+ executor = MendixBridge::MigrationExecutor.new(
240
+ mxcli: File.join(bridge_dir, "bin", "mxcli")
241
+ )
242
+ executor.restore(
243
+ backup_dir,
244
+ project_file: options.fetch(:project),
245
+ confirmation: options.fetch(:confirmation),
246
+ studio_closed: options.fetch(:studio_closed)
247
+ )
248
+ puts "Backup restored successfully: #{backup_dir}"
249
+ when "new"
250
+ options = { git: true }
251
+ parser = OptionParser.new do |opts|
252
+ opts.banner = "Usage: mendix-ruby new APP.rb --output DIR --version VERSION"
253
+ opts.on("-o", "--output DIR", "Create the Mendix project in DIR") do |dir|
254
+ options[:output] = File.expand_path(dir)
255
+ end
256
+ opts.on("--version VERSION", "Mendix version, for example 11.6.8") do |version|
257
+ options[:version] = version
258
+ end
259
+ opts.on("--no-git", "Do not initialize Git or create the initial commit") do
260
+ options[:git] = false
261
+ end
262
+ end
263
+ parser.parse!
264
+ dsl_file = ARGV.shift
265
+ unless dsl_file && options[:output] && options[:version] && ARGV.empty?
266
+ warn parser
267
+ exit 1
268
+ end
269
+
270
+ dsl_file = File.expand_path(dsl_file)
271
+ model = MendixBridge.load_file(dsl_file)
272
+ bridge_dir = File.expand_path("..", __dir__)
273
+ creator = MendixBridge::ProjectCreator.new(
274
+ mxcli: File.join(bridge_dir, "bin", "mxcli")
275
+ )
276
+ result = creator.create(
277
+ model,
278
+ source_file: dsl_file,
279
+ output_dir: options.fetch(:output),
280
+ version: options.fetch(:version),
281
+ git: options.fetch(:git)
282
+ )
283
+ puts "Created Mendix project: #{result.project_file}"
284
+ puts "Ruby model: #{result.model_file}"
285
+ puts "Git initialized with Initial Setup commit." if result.git_initialized
286
+ when "compile"
287
+ options = { pretty: true, format: :json }
288
+ parser = OptionParser.new do |opts|
289
+ opts.banner = "Usage: mendix-ruby compile APP.rb [options]"
290
+
291
+ opts.on("-o", "--output FILE", "Write output to FILE") do |file|
292
+ options[:output] = file
293
+ end
294
+
295
+ opts.on("--compact", "Generate compact JSON") do
296
+ options[:pretty] = false
297
+ end
298
+
299
+ opts.on("-f", "--format FORMAT", %w[json mdl], "Output format: json or mdl") do |format|
300
+ options[:format] = format.to_sym
301
+ end
302
+ end
303
+
304
+ parser.parse!
305
+ input = ARGV.shift || project_config&.model
306
+ unless input && ARGV.empty?
307
+ warn parser
308
+ exit 1
309
+ end
310
+
311
+ model = MendixBridge.load_file(File.expand_path(input))
312
+ output = MendixBridge.compile(model, format: options[:format], pretty: options[:pretty])
313
+ output << "\n" unless output.end_with?("\n")
314
+
315
+ if options[:output]
316
+ File.write(options[:output], output)
317
+ else
318
+ print output
319
+ end
320
+ when "import"
321
+ options = { force: false }
322
+ parser = OptionParser.new do |opts|
323
+ opts.banner = "Usage: mendix-ruby import APP.mpr --output DIR"
324
+
325
+ opts.on("-o", "--output DIR", "Create the Ruby inventory in DIR") do |dir|
326
+ options[:output] = File.expand_path(dir)
327
+ end
328
+
329
+ opts.on("--force", "Refresh an existing imported directory") do
330
+ options[:force] = true
331
+ end
332
+ end
333
+
334
+ parser.parse!
335
+ project_file = ARGV.shift || project_config&.project
336
+ options[:output] ||= project_config&.inventory
337
+ unless project_file && options[:output] && ARGV.empty?
338
+ warn parser
339
+ exit 1
340
+ end
341
+
342
+ output_dir = options.fetch(:output)
343
+ if Dir.exist?(output_dir) && !options[:force] && !Dir.empty?(output_dir)
344
+ abort "Output directory is not empty; use --force to refresh it: #{output_dir}"
345
+ end
346
+
347
+ bridge_dir = File.expand_path("..", __dir__)
348
+ importer = MendixBridge::Importer.new(mxcli: File.join(bridge_dir, "bin", "mxcli"))
349
+ inventory, warnings = importer.import(project_file, output_dir)
350
+
351
+ puts "Imported #{inventory.elements.length} elements into #{output_dir}"
352
+ puts "Types: #{inventory.types.map { |type, count| "#{type}=#{count}" }.join(', ')}"
353
+ unless warnings.empty?
354
+ warn "Could not describe #{warnings.length} elements:"
355
+ warnings.each { |warning| warn " #{warning}" }
356
+ end
357
+ when "refresh"
358
+ parser = OptionParser.new do |opts|
359
+ opts.banner = "Usage: mendix-ruby refresh [IMPORTED_DIR]"
360
+ end
361
+ parser.parse!
362
+ output_dir = File.expand_path(
363
+ ARGV.shift || project_config&.inventory || "."
364
+ )
365
+ unless ARGV.empty?
366
+ warn parser
367
+ exit 1
368
+ end
369
+
370
+ metadata_file = File.join(output_dir, "mendix-project.json")
371
+ snapshot_file = File.join(output_dir, "inventory", "project-tree.json")
372
+ abort "Not an imported Mendix Ruby project: #{output_dir}" unless
373
+ File.file?(metadata_file) && File.file?(snapshot_file)
374
+
375
+ metadata = JSON.parse(File.read(metadata_file))
376
+ project_file = metadata.fetch("source_project")
377
+ old_inventory = MendixBridge::Inventory.load(snapshot_file)
378
+ bridge_dir = File.expand_path("..", __dir__)
379
+ importer = MendixBridge::Importer.new(mxcli: File.join(bridge_dir, "bin", "mxcli"))
380
+ new_inventory, warnings = importer.import(project_file, output_dir)
381
+ changes = MendixBridge::SnapshotDiff.compare(old_inventory, new_inventory)
382
+
383
+ changes_dir = File.join(output_dir, "changes")
384
+ FileUtils.mkdir_p(changes_dir)
385
+ File.write(
386
+ File.join(changes_dir, "latest.json"),
387
+ "#{JSON.pretty_generate(changes.to_h)}\n"
388
+ )
389
+
390
+ if changes.empty?
391
+ puts "No Mendix project changes detected."
392
+ else
393
+ puts "Refresh completed:"
394
+ puts " added: #{changes.added.length}"
395
+ puts " modified: #{changes.modified.length}"
396
+ puts " removed: #{changes.removed.length}"
397
+ changes.to_h.each do |kind, elements|
398
+ elements.each { |element| puts " #{kind.to_s.upcase} #{element.fetch('id')}" }
399
+ end
400
+ end
401
+ unless warnings.empty?
402
+ warn "Could not describe #{warnings.length} elements; see warnings below."
403
+ warnings.each { |warning| warn " #{warning}" }
404
+ end
405
+ when "search"
406
+ options = { json: false }
407
+ parser = OptionParser.new do |opts|
408
+ opts.banner = "Usage: mendix-ruby search QUERY [IMPORTED_DIR] [options]"
409
+ opts.on("-t", "--type TYPE", "Filter by element type") { |type| options[:type] = type }
410
+ opts.on("--json", "Print machine-readable JSON") { options[:json] = true }
411
+ end
412
+ parser.parse!
413
+ query = ARGV.shift
414
+ imported_dir = File.expand_path(
415
+ ARGV.shift || project_config&.inventory || "."
416
+ )
417
+ unless query && ARGV.empty?
418
+ warn parser
419
+ exit 1
420
+ end
421
+
422
+ snapshot_file = File.join(imported_dir, "inventory", "project-tree.json")
423
+ abort "Not an imported Mendix Ruby project: #{imported_dir}" unless File.file?(snapshot_file)
424
+
425
+ inventory = MendixBridge::Inventory.load(snapshot_file)
426
+ results = inventory.search(query)
427
+ results = results.select { |element| element.type == options[:type] } if options[:type]
428
+
429
+ if options[:json]
430
+ puts JSON.pretty_generate(results.map { |element| MendixBridge::Presenter.search_result(element) })
431
+ elsif results.empty?
432
+ puts "No elements found."
433
+ else
434
+ results.each do |element|
435
+ item = MendixBridge::Presenter.search_result(element)
436
+ puts "#{item.fetch('type')}\t#{item.fetch('name')}\t#{item.fetch('path')}"
437
+ end
438
+ end
439
+ when "inspect"
440
+ options = { format: :text }
441
+ parser = OptionParser.new do |opts|
442
+ opts.banner = "Usage: mendix-ruby inspect ELEMENT [IMPORTED_DIR] [options]"
443
+ opts.on("-t", "--type TYPE", "Disambiguate by element type") { |type| options[:type] = type }
444
+ opts.on("--json", "Print the complete element as JSON") { options[:format] = :json }
445
+ opts.on("--mdl", "Print the original MDL") { options[:format] = :mdl }
446
+ end
447
+ parser.parse!
448
+ name = ARGV.shift
449
+ imported_dir = File.expand_path(
450
+ ARGV.shift || project_config&.inventory || "."
451
+ )
452
+ unless name && ARGV.empty?
453
+ warn parser
454
+ exit 1
455
+ end
456
+
457
+ snapshot_file = File.join(imported_dir, "inventory", "project-tree.json")
458
+ abort "Not an imported Mendix Ruby project: #{imported_dir}" unless File.file?(snapshot_file)
459
+
460
+ inventory = MendixBridge::Inventory.load(snapshot_file)
461
+ matches = inventory.elements.select { |candidate| candidate.qualified_name == name }
462
+ matches = matches.select { |candidate| candidate.type == options[:type] } if options[:type]
463
+ abort "Element not found: #{name}" if matches.empty?
464
+ if matches.length > 1
465
+ types = matches.map(&:type).uniq.sort.join(", ")
466
+ abort "Ambiguous element #{name}; choose one with --type (#{types})"
467
+ end
468
+ element = matches.first
469
+
470
+ case options[:format]
471
+ when :json
472
+ puts JSON.pretty_generate(element.to_h)
473
+ when :mdl
474
+ mdl = element.details&.fetch("mdl", nil)
475
+ abort "Original MDL is not available for #{name}" unless mdl
476
+ puts mdl
477
+ else
478
+ puts MendixBridge::Presenter.text(element)
479
+ end
480
+ when "plan"
481
+ options = { json: false }
482
+ parser = OptionParser.new do |opts|
483
+ opts.banner = "Usage: mendix-ruby plan APP.rb --inventory IMPORTED_DIR [--json]"
484
+ opts.on("-i", "--inventory DIR", "Imported Mendix Ruby project") do |dir|
485
+ options[:inventory] = File.expand_path(dir)
486
+ end
487
+ opts.on("--json", "Print machine-readable JSON") { options[:json] = true }
488
+ end
489
+ parser.parse!
490
+ dsl_file = ARGV.shift || project_config&.model
491
+ options[:inventory] ||= project_config&.inventory
492
+ unless dsl_file && options[:inventory] && ARGV.empty?
493
+ warn parser
494
+ exit 1
495
+ end
496
+
497
+ snapshot_file = File.join(options.fetch(:inventory), "inventory", "project-tree.json")
498
+ abort "Not an imported Mendix Ruby project: #{options.fetch(:inventory)}" unless
499
+ File.file?(snapshot_file)
500
+
501
+ model = MendixBridge.load_file(File.expand_path(dsl_file))
502
+ inventory = MendixBridge::Inventory.load(snapshot_file)
503
+ plan = MendixBridge::ChangePlanner.plan(model, inventory)
504
+
505
+ if options[:json]
506
+ puts JSON.pretty_generate(plan.to_h)
507
+ else
508
+ plan.operations.each do |operation|
509
+ puts "#{operation.action.upcase}\t#{operation.type}\t#{operation.name}"
510
+ puts " Reason: #{operation.reason}" if operation.reason
511
+ if operation.changes
512
+ JSON.pretty_generate(operation.changes).each_line { |line| puts " #{line}" }
513
+ end
514
+ end
515
+ puts "Preserved undeclared managed elements: #{plan.preserved_undeclared}"
516
+ puts "Summary: #{plan.counts.map { |action, count| "#{action}=#{count}" }.join(', ')}"
517
+ end
518
+
519
+ exit 2 if plan.blocked?
520
+ when "view"
521
+ options = {}
522
+ parser = OptionParser.new do |opts|
523
+ opts.banner = "Usage: mendix-ruby view [IMPORTED_DIR] [-o OUTPUT.html]"
524
+
525
+ opts.on("-o", "--output FILE", "Write the viewer to FILE") do |file|
526
+ options[:output] = File.expand_path(file)
527
+ end
528
+ end
529
+
530
+ parser.parse!
531
+ imported_dir = File.expand_path(
532
+ ARGV.shift || project_config&.inventory || "."
533
+ )
534
+ unless ARGV.empty?
535
+ warn parser
536
+ exit 1
537
+ end
538
+
539
+ snapshot_file = File.join(imported_dir, "inventory", "project-tree.json")
540
+ abort "Not an imported Mendix Ruby project: #{imported_dir}" unless File.file?(snapshot_file)
541
+
542
+ output = MendixBridge::HtmlViewer.write(imported_dir, options[:output])
543
+ puts "Wrote inventory viewer to #{output}"
544
+ puts "Open it in a browser: file://#{output}"
545
+ when "desktop"
546
+ parser = OptionParser.new do |opts|
547
+ opts.banner = "Usage: mendix-ruby desktop [IMPORTED_DIR]"
548
+ end
549
+ parser.parse!
550
+ inventory_dir = File.expand_path(
551
+ ARGV.shift || project_config&.inventory || "."
552
+ )
553
+ unless ARGV.empty?
554
+ warn parser
555
+ exit 1
556
+ end
557
+
558
+ bridge_dir = File.expand_path("..", __dir__)
559
+ MendixBridge::DesktopApp.new(inventory_dir:, bridge_dir:).run
560
+ else
561
+ warn <<~USAGE
562
+ Usage:
563
+ mendix-ruby setup
564
+ mendix-ruby serve [IMPORTED_DIR] [--bind ADDRESS] [--port PORT]
565
+ mendix-ruby desktop [IMPORTED_DIR]
566
+ mendix-ruby configure --project APP.mpr --inventory DIR --model APP.rb
567
+ mendix-ruby deps ELEMENT [IMPORTED_DIR] [options]
568
+ mendix-ruby migrate MIGRATION.rb --project APP.mpr [--dry-run]
569
+ mendix-ruby rollback BACKUP_DIR --project APP.mpr
570
+ mendix-ruby new APP.rb --output DIR --version VERSION [--no-git]
571
+ mendix-ruby compile APP.rb [options]
572
+ mendix-ruby import APP.mpr --output DIR [--force]
573
+ mendix-ruby refresh [IMPORTED_DIR]
574
+ mendix-ruby search QUERY [IMPORTED_DIR] [--type TYPE] [--json]
575
+ mendix-ruby inspect ELEMENT [IMPORTED_DIR] [--type TYPE] [--json|--mdl]
576
+ mendix-ruby plan APP.rb --inventory IMPORTED_DIR [--json]
577
+ mendix-ruby view [IMPORTED_DIR] [-o OUTPUT.html]
578
+ USAGE
579
+ exit 1
580
+ end
data/bin/mxcli ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ version="$(tr -d '[:space:]' < "$project_dir/.mxcli-version")"
6
+ if [[ -n "${MENDIX_RUBY_TOOLS_DIR:-}" ]]; then
7
+ tools_dir="$MENDIX_RUBY_TOOLS_DIR"
8
+ elif [[ -d "$project_dir/.git" ]]; then
9
+ tools_dir="$project_dir/.tools"
10
+ else
11
+ tools_dir="${XDG_DATA_HOME:-$HOME/.local/share}/mendix-ruby-bridge/tools"
12
+ fi
13
+ executable="$tools_dir/mxcli/$version/mxcli"
14
+
15
+ if [[ ! -x "$executable" ]]; then
16
+ echo "mxcli $version is not installed. Run: mendix-ruby setup" >&2
17
+ exit 1
18
+ fi
19
+
20
+ exec "$executable" "$@"
data/bin/setup-tools ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ version="$(tr -d '[:space:]' < "$project_dir/.mxcli-version")"
6
+ if [[ -n "${MENDIX_RUBY_TOOLS_DIR:-}" ]]; then
7
+ tools_dir="$MENDIX_RUBY_TOOLS_DIR"
8
+ elif [[ -d "$project_dir/.git" ]]; then
9
+ tools_dir="$project_dir/.tools"
10
+ else
11
+ tools_dir="${XDG_DATA_HOME:-$HOME/.local/share}/mendix-ruby-bridge/tools"
12
+ fi
13
+ install_dir="$tools_dir/mxcli/$version"
14
+ executable="$install_dir/mxcli"
15
+ download_url="https://github.com/mendixlabs/mxcli/releases/download/v${version}/mxcli-linux-amd64"
16
+
17
+ if [[ -x "$executable" ]]; then
18
+ echo "mxcli $version is already installed"
19
+ exit 0
20
+ fi
21
+
22
+ mkdir -p "$install_dir"
23
+ curl --fail --location "$download_url" --output "$executable"
24
+ chmod +x "$executable"
25
+ "$executable" --version