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.
- checksums.yaml +7 -0
- data/.mxcli-version +2 -0
- data/LICENSE +21 -0
- data/README.md +77 -0
- data/bin/git-mendix +4 -0
- data/bin/mendix-apply +145 -0
- data/bin/mendix-desktop +26 -0
- data/bin/mendix-git +121 -0
- data/bin/mendix-ruby +580 -0
- data/bin/mxcli +20 -0
- data/bin/setup-tools +25 -0
- data/lib/mendix_bridge/backend_server.rb +1404 -0
- data/lib/mendix_bridge/change_planner.rb +594 -0
- data/lib/mendix_bridge/config.rb +89 -0
- data/lib/mendix_bridge/dependency_index.rb +188 -0
- data/lib/mendix_bridge/desktop_app.rb +121 -0
- data/lib/mendix_bridge/document_parser.rb +120 -0
- data/lib/mendix_bridge/domain_parser.rb +103 -0
- data/lib/mendix_bridge/dsl.rb +540 -0
- data/lib/mendix_bridge/enumeration_parser.rb +32 -0
- data/lib/mendix_bridge/git_workflow.rb +321 -0
- data/lib/mendix_bridge/html_viewer.rb +672 -0
- data/lib/mendix_bridge/importer.rb +415 -0
- data/lib/mendix_bridge/inventory.rb +93 -0
- data/lib/mendix_bridge/mdl_generator.rb +343 -0
- data/lib/mendix_bridge/microflow_parser.rb +131 -0
- data/lib/mendix_bridge/migration.rb +290 -0
- data/lib/mendix_bridge/migration_executor.rb +234 -0
- data/lib/mendix_bridge/model.rb +204 -0
- data/lib/mendix_bridge/page_parser.rb +95 -0
- data/lib/mendix_bridge/presenter.rb +35 -0
- data/lib/mendix_bridge/project_creator.rb +181 -0
- data/lib/mendix_bridge/ruby_inventory_generator.rb +63 -0
- data/lib/mendix_bridge/security_parser.rb +72 -0
- data/lib/mendix_bridge/snapshot_diff.rb +58 -0
- data/lib/mendix_bridge/validator.rb +46 -0
- data/lib/mendix_bridge/version.rb +5 -0
- data/lib/mendix_bridge/visual_entity_plan.rb +176 -0
- data/lib/mendix_bridge.rb +127 -0
- data/share/applications/mendix-ruby-bridge.desktop +12 -0
- data/share/icons/hicolor/128x128/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/256x256/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/32x32/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/48x48/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/512x512/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/64x64/apps/mendix-ruby-bridge.png +0 -0
- data/web/dist/apple-touch-icon.png +0 -0
- data/web/dist/assets/index-BO6iPT1P.css +1 -0
- data/web/dist/assets/index-JGODw81W.js +27 -0
- data/web/dist/brand/mendix-ruby-bridge.png +0 -0
- data/web/dist/favicon.png +0 -0
- data/web/dist/icons.svg +24 -0
- data/web/dist/index.html +17 -0
- 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
|