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
|
@@ -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
|