mxrb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module Semantic
5
+ # Deterministic hashed term-frequency embeddings with unit normalization.
6
+ class TfidfEmbedder
7
+ DIM = 512
8
+
9
+ def dimension = DIM
10
+ def backend = :tfidf
11
+
12
+ def embed(text)
13
+ tokens = tokenize(text)
14
+ return Array.new(DIM, 0.0) if tokens.empty?
15
+
16
+ freq = tokens.tally
17
+ total = tokens.size.to_f
18
+ vec = Array.new(DIM, 0.0)
19
+ freq.each { |term, count| vec[bucket(term)] += count / total }
20
+ normalize(vec)
21
+ end
22
+
23
+ private
24
+
25
+ # FNV-1a 32-bit — deterministic across Ruby versions and RUBY_HASH_SEED
26
+ def fnv32(str)
27
+ h = 0x811c9dc5
28
+ str.each_byte { |b| h = ((h ^ b) * 0x01000193) & 0xFFFFFFFF }
29
+ h
30
+ end
31
+
32
+ def bucket(term) = fnv32(term) % DIM
33
+
34
+ def tokenize(text)
35
+ text.downcase.scan(/[a-z][a-z0-9]*/).reject { _1.length < 2 }
36
+ end
37
+
38
+ def normalize(vec)
39
+ norm = Math.sqrt(vec.sum { _1**2 })
40
+ return vec if norm.zero?
41
+
42
+ vec.map { _1 / norm }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'embedder'
5
+
6
+ module Mxrb
7
+ module Semantic
8
+ # Manages the _MxrbVecIndex virtual table (sqlite-vec) inside an .mpr file.
9
+ # Optional: if the sqlite-vec gem is not installed this object is never created
10
+ # and Index falls back to regex search transparently.
11
+ class VecStore
12
+ META_TABLE = '_MxrbVecMeta'
13
+ VEC_TABLE = '_MxrbVecIndex'
14
+
15
+ def self.available?
16
+ require 'sqlite_vec'
17
+ true
18
+ rescue LoadError
19
+ false
20
+ end
21
+
22
+ def initialize(mpr, embedder:, fingerprint:)
23
+ @mpr = mpr
24
+ @embedder = embedder
25
+ @fingerprint = fingerprint
26
+ mpr.load_vec_extension!
27
+ ensure_tables!
28
+ end
29
+
30
+ def backend = @embedder.backend
31
+ def dimension = @embedder.dimension
32
+
33
+ # Replaces all vectors and marks the table with its source fingerprint.
34
+ def rebuild!(artifacts)
35
+ @mpr.vec_drop_index!(VEC_TABLE, META_TABLE)
36
+ ensure_tables!
37
+ @mpr.vec_transaction do
38
+ artifacts.each { upsert(_1.id, Embedder.artifact_text(_1)) }
39
+ @mpr.write_vec_meta!(META_TABLE, backend.to_s, dimension, @fingerprint)
40
+ end
41
+ self
42
+ end
43
+
44
+ # K-nearest-neighbour search. Returns [{id:, distance:}] sorted by distance.
45
+ def search(query, limit: 10)
46
+ vec = @embedder.embed(query)
47
+ json = JSON.generate(vec)
48
+ @mpr.vec_knn(VEC_TABLE, json, limit)
49
+ end
50
+
51
+ # True when vectors represent this exact model and embedding backend.
52
+ def compatible?
53
+ stored = @mpr.vec_meta(META_TABLE)
54
+ return false unless stored
55
+
56
+ stored == {
57
+ backend: backend.to_s, dimension:, fingerprint: @fingerprint
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ def ensure_tables!
64
+ @mpr.ensure_vec_table!(VEC_TABLE, dimension)
65
+ @mpr.ensure_vec_meta_table!(META_TABLE)
66
+ end
67
+
68
+ def upsert(id, text)
69
+ vec = @embedder.embed(text)
70
+ json = JSON.generate(vec)
71
+ @mpr.vec_upsert(VEC_TABLE, id, json)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,390 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'open3'
6
+ require 'tmpdir'
7
+ require 'uri'
8
+
9
+ module Mxrb
10
+ module TeamServer
11
+ HOST = 'git.api.mendix.com'
12
+ API_BASE = 'https://repository.api.mendix.com/v1'
13
+ APP_ID = /\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i
14
+
15
+ CloneResult = Data.define(:root, :repository_url, :branch, :mpr_files)
16
+ GitResult = Data.define(:output, :repository_url)
17
+
18
+ # Stores only a reference to a user-managed PAT file.
19
+ class Credentials
20
+ attr_reader :path
21
+
22
+ def initialize(path: nil, pat_file: ENV['MXRB_TEAM_SERVER_PAT_FILE'])
23
+ base = ENV['XDG_CONFIG_HOME'].to_s
24
+ base = File.join(Dir.home, '.config') if base.empty?
25
+ @path = File.expand_path(path || File.join(base, 'mxrb', 'credentials'))
26
+ @pat_file = expand(pat_file)
27
+ end
28
+
29
+ def configure_pat_file(source)
30
+ @pat_file = expand(source)
31
+ raise TeamServerError, 'Team Server PAT file path must not be empty' unless @pat_file
32
+
33
+ read_pat_file(@pat_file)
34
+ config = File.file?(@path) ? read_config : {}
35
+ config.delete('team_server_pat')
36
+ write_config(config.merge('team_server_pat_file' => @pat_file))
37
+ end
38
+
39
+ def pat
40
+ source = @pat_file
41
+ source ||= expand(read_config['team_server_pat_file']) if File.file?(@path)
42
+ source && read_pat_file(source)
43
+ end
44
+
45
+ private
46
+
47
+ def expand(value)
48
+ value = value.to_s.strip
49
+ value.empty? ? nil : File.expand_path(value)
50
+ end
51
+
52
+ def read_config
53
+ value = JSON.parse(File.binread(@path))
54
+ raise TeamServerError, 'invalid credentials file: expected a JSON object' unless value.is_a?(Hash)
55
+
56
+ value
57
+ rescue JSON::ParserError => e
58
+ raise TeamServerError, "invalid credentials file: #{e.message}"
59
+ end
60
+
61
+ def write_config(value)
62
+ FileUtils.mkdir_p(File.dirname(@path), mode: 0o700)
63
+ temporary = "#{@path}.tmp-#{Process.pid}"
64
+ File.binwrite(temporary, JSON.generate(value) << "\n")
65
+ File.chmod(0o600, temporary)
66
+ File.rename(temporary, @path)
67
+ @path
68
+ ensure
69
+ FileUtils.rm_f(temporary) if defined?(temporary) && temporary
70
+ end
71
+
72
+ def read_pat_file(source)
73
+ raise TeamServerError, "Team Server PAT file not found: #{source}" unless File.file?(source)
74
+
75
+ raw = File.binread(source).strip
76
+ value = extract_pat(raw, source)
77
+ raise TeamServerError, "Team Server PAT file is empty: #{source}" if value.empty?
78
+
79
+ value
80
+ rescue SystemCallError => e
81
+ raise TeamServerError, "cannot read Team Server PAT file #{source}: #{e.message}"
82
+ end
83
+
84
+ def extract_pat(raw, source)
85
+ return extract_env_pat(raw, source) if env_file?(raw, source)
86
+
87
+ parsed = JSON.parse(raw)
88
+ return parsed.fetch('team_server_pat', parsed['mendix_pat']).to_s.strip if parsed.is_a?(Hash)
89
+
90
+ raise TeamServerError, 'JSON Team Server PAT file must contain team_server_pat'
91
+ rescue JSON::ParserError
92
+ raw
93
+ end
94
+
95
+ def env_file?(raw, source)
96
+ File.basename(source).start_with?('.env') || raw.match?(/MXRB_(?:TEAM_SERVER|MENDIX)_PAT\s*=/)
97
+ end
98
+
99
+ def extract_env_pat(raw, source)
100
+ pattern = /^\s*(?:export\s+)?MXRB_(?:TEAM_SERVER|MENDIX)_PAT\s*=/
101
+ line = raw.lines.reverse.find { _1.match?(pattern) }
102
+ raise TeamServerError, "Team Server PAT variable not found in #{source}" unless line
103
+
104
+ unquote(line.sub(pattern, '').strip)
105
+ end
106
+
107
+ def unquote(value)
108
+ return value unless value.length >= 2 && %w[' "].include?(value[0]) && value[-1] == value[0]
109
+
110
+ value[1...-1]
111
+ end
112
+ end
113
+
114
+ # Injectable subprocess boundary used by the Git transport.
115
+ class CommandRunner
116
+ def capture(environment, command, chdir: nil)
117
+ options = chdir ? { chdir: } : {}
118
+ Open3.capture2e(environment, *command, **options)
119
+ end
120
+ end
121
+
122
+ # Provides Git's credentials through an ephemeral askpass script.
123
+ class GitAuthenticator
124
+ def initialize(credentials)
125
+ @credentials = credentials
126
+ end
127
+
128
+ def call
129
+ token = @credentials.pat
130
+ return yield('GIT_TERMINAL_PROMPT' => '0') unless token
131
+
132
+ Dir.mktmpdir('mxrb-askpass-') do |dir|
133
+ helper = write_helper(dir)
134
+ yield authenticated_environment(helper, token)
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def write_helper(dir)
141
+ helper = File.join(dir, 'askpass')
142
+ File.write(helper, <<~SH)
143
+ #!/bin/sh
144
+ case "$1" in
145
+ *Username*) printf '%s\\n' pat ;;
146
+ *Password*) printf '%s\\n' "$MXRB_TEAM_SERVER_PAT" ;;
147
+ *) exit 1 ;;
148
+ esac
149
+ SH
150
+ File.chmod(0o700, helper)
151
+ helper
152
+ end
153
+
154
+ def authenticated_environment(helper, token)
155
+ {
156
+ 'GIT_ASKPASS' => helper, 'GIT_ASKPASS_REQUIRE' => 'force',
157
+ 'GIT_TERMINAL_PROMPT' => '0', 'MXRB_TEAM_SERVER_PAT' => token
158
+ }
159
+ end
160
+ end
161
+
162
+ # Git transport for Team Server. PATs are passed to a short-lived
163
+ # GIT_ASKPASS process and are never embedded in a URL or git config.
164
+ class Repository
165
+ def initialize(credentials: Credentials.new, runner: CommandRunner.new,
166
+ authenticator: GitAuthenticator.new(credentials))
167
+ @runner = runner
168
+ @authenticator = authenticator
169
+ end
170
+
171
+ def clone(source, target:, branch: nil, depth: nil)
172
+ url = self.class.repository_url(source)
173
+ root = File.expand_path(target)
174
+ raise TeamServerError, "#{root}: destination already exists" if File.exist?(root)
175
+
176
+ capture!(clone_arguments(url, root, branch:, depth:))
177
+ mprs = postprocess_clone(root)
178
+ CloneResult.new(root:, repository_url: url, branch:, mpr_files: mprs)
179
+ rescue StandardError
180
+ FileUtils.rm_rf(root) if defined?(root) && root && File.directory?(root) && !File.exist?(File.join(root,
181
+ '.git'))
182
+ raise
183
+ end
184
+
185
+ def fetch(root, prune: true)
186
+ repository = repository_root(root)
187
+ args = %w[git fetch]
188
+ args << '--prune' if prune
189
+ output = capture!(args, chdir: repository)
190
+ GitResult.new(output:, repository_url: remote_url(repository))
191
+ end
192
+
193
+ def pull(root, ff_only: true)
194
+ repository = repository_root(root)
195
+ args = %w[git pull]
196
+ args << '--ff-only' if ff_only
197
+ output = capture!(args, chdir: repository)
198
+ validate_mprs!(repository)
199
+ GitResult.new(output:, repository_url: remote_url(repository))
200
+ end
201
+
202
+ def push(root, remote: 'origin', branch: nil)
203
+ repository = repository_root(root)
204
+ args = ['git', 'push', '--', remote.to_s]
205
+ args << branch.to_s if branch
206
+ output = capture!(args, chdir: repository)
207
+ GitResult.new(output:, repository_url: remote_url(repository, remote:))
208
+ end
209
+
210
+ def status(root)
211
+ repository = repository_root(root)
212
+ capture!(['git', 'status', '--short', '--branch'], chdir: repository)
213
+ end
214
+
215
+ def self.repository_url(source)
216
+ value = source.to_s.strip
217
+ value = "https://#{HOST}/#{value}.git" if value.match?(APP_ID)
218
+ uri = URI.parse(value)
219
+ raise TeamServerError, "invalid Team Server Git URL: #{source.inspect}" unless valid_repository_uri?(uri)
220
+
221
+ uri.to_s
222
+ rescue URI::InvalidURIError
223
+ raise TeamServerError, "invalid Team Server Git URL: #{source.inspect}"
224
+ end
225
+
226
+ private
227
+
228
+ def self.valid_repository_uri?(uri)
229
+ uri.scheme == 'https' && uri.host == HOST && uri.userinfo.nil? &&
230
+ uri.path.match?(%r{\A/[0-9a-f-]{36}\.git\z}i) && uri.query.nil? && uri.fragment.nil?
231
+ end
232
+
233
+ private_class_method :valid_repository_uri?
234
+
235
+ def clone_arguments(url, root, branch:, depth:)
236
+ arguments = %w[git clone]
237
+ arguments += ['--branch', branch.to_s] if branch
238
+ arguments += ['--depth', positive_depth(depth).to_s] if depth
239
+ arguments + ['--', url, root]
240
+ end
241
+
242
+ def positive_depth(value)
243
+ depth = Integer(value)
244
+ raise ArgumentError unless depth.positive?
245
+
246
+ depth
247
+ rescue ArgumentError, TypeError
248
+ raise TeamServerError, 'clone depth must be a positive integer'
249
+ end
250
+
251
+ def capture!(command, chdir: nil)
252
+ @authenticator.call do |environment|
253
+ output, status = @runner.capture(environment, command, chdir:)
254
+ raise TeamServerError, "Team Server Git operation failed: #{output.strip}" unless status.success?
255
+
256
+ output
257
+ end
258
+ end
259
+
260
+ def repository_root(root)
261
+ path = File.expand_path(root)
262
+ raise TeamServerError, "#{path}: not a Git repository" unless File.directory?(File.join(path, '.git'))
263
+
264
+ self.class.repository_url(remote_url(path))
265
+ path
266
+ end
267
+
268
+ def remote_url(root, remote: 'origin')
269
+ output = capture!(['git', 'remote', 'get-url', remote.to_s], chdir: root).strip
270
+ self.class.repository_url(output)
271
+ end
272
+
273
+ def postprocess_clone(root)
274
+ mprs = validate_mprs!(root)
275
+ raise TeamServerError, 'clone contains no MPR' if mprs.empty?
276
+
277
+ mprs
278
+ end
279
+
280
+ def validate_mprs!(root)
281
+ Dir.glob(File.join(root, '*.mpr')).sort.map do |mpr|
282
+ result = Mxrb.validate(mpr)
283
+ raise TeamServerError, "cloned MPR is invalid: #{result.errors.first}" unless result.valid?
284
+
285
+ mpr
286
+ end
287
+ end
288
+ end
289
+
290
+ # Read-only client for Mendix's official App Repository API.
291
+ class Api
292
+ def initialize(credentials: Credentials.new, client: OfficialMarketplace::HttpClient.new)
293
+ @credentials = credentials
294
+ @client = client
295
+ end
296
+
297
+ def info(app_id) = request("/repositories/#{id(app_id)}/info")
298
+
299
+ def branches(app_id, limit: 100, cursor: nil)
300
+ request("/repositories/#{id(app_id)}/branches", limit:, cursor:)
301
+ end
302
+
303
+ def branch(app_id, name)
304
+ request("/repositories/#{id(app_id)}/branches/#{escape(name)}")
305
+ end
306
+
307
+ def commits(app_id, name, limit: 100, cursor: nil)
308
+ request("/repositories/#{id(app_id)}/branches/#{escape(name)}/commits", limit:, cursor:)
309
+ end
310
+
311
+ private
312
+
313
+ def request(path, parameters = {})
314
+ token = @credentials.pat.to_s.strip
315
+ if token.empty?
316
+ raise TeamServerError,
317
+ 'Team Server API requires --pat-file or MXRB_TEAM_SERVER_PAT_FILE'
318
+ end
319
+ query = URI.encode_www_form(parameters.compact)
320
+ url = query.empty? ? "#{API_BASE}#{path}" : "#{API_BASE}#{path}?#{query}"
321
+ @client.json(url, authorization: "MxToken #{token}")
322
+ rescue MarketplaceError => e
323
+ raise TeamServerError, e.message
324
+ end
325
+
326
+ def id(value)
327
+ identifier = value.to_s
328
+ raise TeamServerError, 'invalid Team Server app ID' unless identifier.match?(APP_ID)
329
+
330
+ identifier
331
+ end
332
+
333
+ def escape(value)
334
+ URI.encode_www_form_component(value.to_s).gsub('+', '%20')
335
+ end
336
+ end
337
+
338
+ # Read-only paginated client for Mendix's official Projects API.
339
+ class ProjectsApi
340
+ BASE = 'https://projects-api.home.mendix.com/v2/projects?limit=100'
341
+ HOST = 'projects-api.home.mendix.com'
342
+ MAX_PAGES = 100
343
+
344
+ def initialize(credentials: Credentials.new, client: OfficialMarketplace::HttpClient.new)
345
+ @credentials = credentials
346
+ @client = client
347
+ end
348
+
349
+ def all
350
+ projects = []
351
+ each_page { projects.concat(Array(_1['items'])) }
352
+ projects.freeze
353
+ rescue MarketplaceError => e
354
+ raise TeamServerError, e.message
355
+ end
356
+
357
+ private
358
+
359
+ def each_page
360
+ url = BASE
361
+ MAX_PAGES.times do
362
+ payload = @client.json(valid_url(url), authorization: authorization)
363
+ yield payload
364
+ url = payload.dig('links', 'next')
365
+ return unless url
366
+ end
367
+ raise TeamServerError, 'Projects API pagination exceeded its safety limit'
368
+ end
369
+
370
+ def authorization
371
+ token = @credentials.pat.to_s.strip
372
+ raise TeamServerError, 'Projects API requires --pat-file or MXRB_TEAM_SERVER_PAT_FILE' if token.empty?
373
+
374
+ "MxToken #{token}"
375
+ end
376
+
377
+ def valid_url(value)
378
+ uri = URI.parse(value.to_s)
379
+ path = %r{\A/v2/(?:projects|accounts/[^/]+/projects)\z}
380
+ unless uri.is_a?(URI::HTTPS) && uri.host == HOST && uri.path.match?(path)
381
+ raise TeamServerError, "unsafe Projects API pagination URL #{value.inspect}"
382
+ end
383
+
384
+ uri.to_s
385
+ rescue URI::InvalidURIError
386
+ raise TeamServerError, "unsafe Projects API pagination URL #{value.inspect}"
387
+ end
388
+ end
389
+ end
390
+ end