t-ruby 0.0.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.
@@ -0,0 +1,735 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "net/http"
6
+ require "uri"
7
+
8
+ module TRuby
9
+ # Semantic version parsing and comparison
10
+ class SemanticVersion
11
+ include Comparable
12
+
13
+ attr_reader :major, :minor, :patch, :prerelease
14
+
15
+ VERSION_REGEX = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.freeze
16
+
17
+ def initialize(version_string)
18
+ match = VERSION_REGEX.match(version_string.to_s)
19
+ raise ArgumentError, "Invalid version: #{version_string}" unless match
20
+
21
+ @major = match[1].to_i
22
+ @minor = match[2].to_i
23
+ @patch = match[3].to_i
24
+ @prerelease = match[4]
25
+ end
26
+
27
+ def <=>(other)
28
+ return nil unless other.is_a?(SemanticVersion)
29
+
30
+ result = [@major, @minor, @patch] <=> [other.major, other.minor, other.patch]
31
+ return result unless result.zero?
32
+
33
+ # Both have same version, compare prerelease
34
+ return 0 if @prerelease.nil? && other.prerelease.nil?
35
+ return 1 if @prerelease.nil? # Release > prerelease
36
+ return -1 if other.prerelease.nil?
37
+
38
+ @prerelease <=> other.prerelease
39
+ end
40
+
41
+ def satisfies?(constraint)
42
+ VersionConstraint.new(constraint).satisfied_by?(self)
43
+ end
44
+
45
+ def to_s
46
+ base = "#{@major}.#{@minor}.#{@patch}"
47
+ @prerelease ? "#{base}-#{@prerelease}" : base
48
+ end
49
+
50
+ def self.parse(str)
51
+ new(str)
52
+ rescue ArgumentError
53
+ nil
54
+ end
55
+ end
56
+
57
+ # Version constraint (^1.0.0, ~>1.0, >=1.0.0 <2.0.0)
58
+ class VersionConstraint
59
+ attr_reader :constraints
60
+
61
+ def initialize(constraint_string)
62
+ @constraints = parse_constraints(constraint_string)
63
+ end
64
+
65
+ def satisfied_by?(version)
66
+ version = SemanticVersion.new(version) if version.is_a?(String)
67
+ @constraints.all? { |op, target| check_constraint(version, op, target) }
68
+ end
69
+
70
+ private
71
+
72
+ def parse_constraints(str)
73
+ constraints = []
74
+ parts = str.split(/\s+/)
75
+
76
+ i = 0
77
+ while i < parts.length
78
+ part = parts[i]
79
+
80
+ case part
81
+ when /^\^(.+)$/ # Caret range: ^1.2.3 means >=1.2.3 <2.0.0
82
+ version = SemanticVersion.new(Regexp.last_match(1))
83
+ constraints << [:>=, version]
84
+ constraints << [:<, SemanticVersion.new("#{version.major + 1}.0.0")]
85
+ when /^~(.+)$/, /^~>(.+)$/ # Tilde range: ~1.2.3 means >=1.2.3 <1.3.0
86
+ version = SemanticVersion.new(Regexp.last_match(1))
87
+ constraints << [:>=, version]
88
+ constraints << [:<, SemanticVersion.new("#{version.major}.#{version.minor + 1}.0")]
89
+ when /^>=(.+)$/
90
+ constraints << [:>=, SemanticVersion.new(Regexp.last_match(1))]
91
+ when /^<=(.+)$/
92
+ constraints << [:<=, SemanticVersion.new(Regexp.last_match(1))]
93
+ when /^>(.+)$/
94
+ constraints << [:>, SemanticVersion.new(Regexp.last_match(1))]
95
+ when /^<(.+)$/
96
+ constraints << [:<, SemanticVersion.new(Regexp.last_match(1))]
97
+ when /^=(.+)$/, /^(\d+\.\d+\.\d+.*)$/
98
+ constraints << [:==, SemanticVersion.new(Regexp.last_match(1))]
99
+ when "*"
100
+ # Match any version
101
+ end
102
+
103
+ i += 1
104
+ end
105
+
106
+ constraints
107
+ end
108
+
109
+ def check_constraint(version, operator, target)
110
+ version.send(operator, target)
111
+ end
112
+ end
113
+
114
+ # Package manifest (.trb-manifest.json)
115
+ class PackageManifest
116
+ MANIFEST_FILE = ".trb-manifest.json"
117
+
118
+ attr_accessor :name, :version, :description, :author, :license
119
+ attr_accessor :types, :dependencies, :dev_dependencies
120
+ attr_accessor :repository, :keywords, :main
121
+
122
+ def initialize(data = {})
123
+ @name = data[:name] || data["name"]
124
+ @version = data[:version] || data["version"] || "0.0.0"
125
+ @description = data[:description] || data["description"]
126
+ @author = data[:author] || data["author"]
127
+ @license = data[:license] || data["license"]
128
+ @types = data[:types] || data["types"] || "lib/types/**/*.d.trb"
129
+ @dependencies = data[:dependencies] || data["dependencies"] || {}
130
+ @dev_dependencies = data[:dev_dependencies] || data["devDependencies"] || {}
131
+ @repository = data[:repository] || data["repository"]
132
+ @keywords = data[:keywords] || data["keywords"] || []
133
+ @main = data[:main] || data["main"]
134
+ end
135
+
136
+ def to_h
137
+ {
138
+ name: @name,
139
+ version: @version,
140
+ description: @description,
141
+ author: @author,
142
+ license: @license,
143
+ types: @types,
144
+ dependencies: @dependencies,
145
+ devDependencies: @dev_dependencies,
146
+ repository: @repository,
147
+ keywords: @keywords,
148
+ main: @main
149
+ }.compact
150
+ end
151
+
152
+ def to_json(*args)
153
+ JSON.pretty_generate(to_h)
154
+ end
155
+
156
+ def save(path = MANIFEST_FILE)
157
+ File.write(path, to_json)
158
+ end
159
+
160
+ def self.load(path = MANIFEST_FILE)
161
+ return nil unless File.exist?(path)
162
+
163
+ data = JSON.parse(File.read(path))
164
+ new(data)
165
+ rescue JSON::ParserError
166
+ nil
167
+ end
168
+
169
+ def valid?
170
+ !@name.nil? && !@name.empty? && !@version.nil?
171
+ end
172
+
173
+ def add_dependency(name, version)
174
+ @dependencies[name] = version
175
+ end
176
+
177
+ def add_dev_dependency(name, version)
178
+ @dev_dependencies[name] = version
179
+ end
180
+
181
+ def remove_dependency(name)
182
+ @dependencies.delete(name)
183
+ end
184
+ end
185
+
186
+ # Dependency resolver
187
+ class DependencyResolver
188
+ attr_reader :resolved, :conflicts
189
+
190
+ def initialize(registry = nil)
191
+ @registry = registry || PackageRegistry.new
192
+ @resolved = {}
193
+ @conflicts = []
194
+ @in_progress = Set.new
195
+ end
196
+
197
+ # Resolve all dependencies for a manifest
198
+ def resolve(manifest)
199
+ @resolved = {}
200
+ @conflicts = []
201
+
202
+ manifest.dependencies.each do |name, version_constraint|
203
+ resolve_package(name, version_constraint)
204
+ end
205
+
206
+ { resolved: @resolved, conflicts: @conflicts }
207
+ end
208
+
209
+ # Check for circular dependencies
210
+ def check_circular(manifest)
211
+ visited = Set.new
212
+ path = []
213
+
214
+ check_circular_recursive(manifest.name, manifest.dependencies, visited, path)
215
+ end
216
+
217
+ private
218
+
219
+ def resolve_package(name, constraint)
220
+ return if @resolved.key?(name)
221
+
222
+ if @in_progress.include?(name)
223
+ @conflicts << "Circular dependency detected: #{name}"
224
+ return
225
+ end
226
+
227
+ @in_progress.add(name)
228
+
229
+ # Find matching version
230
+ available = @registry.get_versions(name)
231
+ matching = find_matching_version(available, constraint)
232
+
233
+ if matching
234
+ @resolved[name] = matching
235
+
236
+ # Resolve transitive dependencies
237
+ pkg_info = @registry.get_package(name, matching)
238
+ if pkg_info && pkg_info[:dependencies]
239
+ pkg_info[:dependencies].each do |dep_name, dep_constraint|
240
+ resolve_package(dep_name, dep_constraint)
241
+ end
242
+ end
243
+ else
244
+ @conflicts << "No matching version for #{name} (#{constraint})"
245
+ end
246
+
247
+ @in_progress.delete(name)
248
+ end
249
+
250
+ def find_matching_version(versions, constraint)
251
+ constraint_obj = VersionConstraint.new(constraint)
252
+ versions
253
+ .map { |v| SemanticVersion.parse(v) }
254
+ .compact
255
+ .select { |v| constraint_obj.satisfied_by?(v) }
256
+ .max
257
+ &.to_s
258
+ end
259
+
260
+ def check_circular_recursive(name, deps, visited, path)
261
+ return [] if deps.nil? || deps.empty?
262
+
263
+ if path.include?(name)
264
+ cycle_start = path.index(name)
265
+ return [path[cycle_start..] + [name]]
266
+ end
267
+
268
+ return [] if visited.include?(name)
269
+
270
+ visited.add(name)
271
+ path.push(name)
272
+
273
+ cycles = []
274
+ deps.each_key do |dep_name|
275
+ pkg = @registry.get_package(dep_name, "*")
276
+ if pkg
277
+ cycles.concat(check_circular_recursive(dep_name, pkg[:dependencies] || {}, visited, path.dup))
278
+ end
279
+ end
280
+
281
+ path.pop
282
+ cycles
283
+ end
284
+ end
285
+
286
+ # Remote registry client (RubyGems.org style API)
287
+ class RemoteRegistry
288
+ DEFAULT_REGISTRY_URL = "https://rubygems.org/api/v1"
289
+ TYPE_REGISTRY_URL = "https://types.ruby-lang.org/api/v1" # Hypothetical type registry
290
+
291
+ attr_reader :registry_url, :cache_dir
292
+
293
+ def initialize(registry_url: nil, cache_dir: nil)
294
+ @registry_url = registry_url || TYPE_REGISTRY_URL
295
+ @cache_dir = cache_dir || File.join(Dir.home, ".trb-cache")
296
+ @http_cache = {}
297
+ FileUtils.mkdir_p(@cache_dir)
298
+ end
299
+
300
+ # Search for type packages
301
+ def search(query, page: 1, per_page: 30)
302
+ uri = URI("#{@registry_url}/search.json")
303
+ uri.query = URI.encode_www_form(query: query, page: page, per_page: per_page)
304
+
305
+ response = fetch_json(uri)
306
+ return [] unless response
307
+
308
+ response.map do |pkg|
309
+ {
310
+ name: pkg["name"],
311
+ version: pkg["version"],
312
+ downloads: pkg["downloads"],
313
+ summary: pkg["info"] || pkg["summary"]
314
+ }
315
+ end
316
+ rescue StandardError => e
317
+ warn "Registry search failed: #{e.message}"
318
+ []
319
+ end
320
+
321
+ # Get package info
322
+ def info(name)
323
+ uri = URI("#{@registry_url}/gems/#{name}.json")
324
+ response = fetch_json(uri)
325
+ return nil unless response
326
+
327
+ {
328
+ name: response["name"],
329
+ version: response["version"],
330
+ authors: response["authors"],
331
+ summary: response["info"],
332
+ homepage: response["homepage_uri"],
333
+ source_code: response["source_code_uri"],
334
+ documentation: response["documentation_uri"],
335
+ licenses: response["licenses"],
336
+ dependencies: parse_dependencies(response["dependencies"])
337
+ }
338
+ rescue StandardError => e
339
+ warn "Failed to fetch package info: #{e.message}"
340
+ nil
341
+ end
342
+
343
+ # Get all versions of a package
344
+ def versions(name)
345
+ uri = URI("#{@registry_url}/versions/#{name}.json")
346
+ response = fetch_json(uri)
347
+ return [] unless response
348
+
349
+ response.map do |v|
350
+ {
351
+ number: v["number"],
352
+ created_at: v["created_at"],
353
+ prerelease: v["prerelease"],
354
+ sha: v["sha"]
355
+ }
356
+ end
357
+ rescue StandardError => e
358
+ warn "Failed to fetch versions: #{e.message}"
359
+ []
360
+ end
361
+
362
+ # Download package
363
+ def download(name, version, target_dir = nil)
364
+ target = target_dir || File.join(@cache_dir, name, version)
365
+ FileUtils.mkdir_p(target)
366
+
367
+ # Download from registry
368
+ uri = URI("#{@registry_url}/gems/#{name}-#{version}.gem")
369
+
370
+ gem_path = File.join(target, "#{name}-#{version}.gem")
371
+ download_file(uri, gem_path)
372
+
373
+ # Extract type definitions
374
+ extract_types(gem_path, target)
375
+
376
+ target
377
+ rescue StandardError => e
378
+ warn "Download failed: #{e.message}"
379
+ nil
380
+ end
381
+
382
+ # Push package to registry
383
+ def push(gem_path, api_key:)
384
+ uri = URI("#{@registry_url}/gems")
385
+
386
+ request = Net::HTTP::Post.new(uri)
387
+ request["Authorization"] = api_key
388
+ request["Content-Type"] = "application/octet-stream"
389
+ request.body = File.binread(gem_path)
390
+
391
+ response = send_request(uri, request)
392
+
393
+ case response
394
+ when Net::HTTPSuccess
395
+ { success: true, message: response.body }
396
+ else
397
+ { success: false, message: response.body }
398
+ end
399
+ rescue StandardError => e
400
+ { success: false, message: e.message }
401
+ end
402
+
403
+ # Yank (unpublish) a version
404
+ def yank(name, version, api_key:)
405
+ uri = URI("#{@registry_url}/gems/yank")
406
+
407
+ request = Net::HTTP::Delete.new(uri)
408
+ request["Authorization"] = api_key
409
+ request.set_form_data(gem_name: name, version: version)
410
+
411
+ response = send_request(uri, request)
412
+
413
+ case response
414
+ when Net::HTTPSuccess
415
+ { success: true }
416
+ else
417
+ { success: false, message: response.body }
418
+ end
419
+ rescue StandardError => e
420
+ { success: false, message: e.message }
421
+ end
422
+
423
+ # Get API key info
424
+ def api_key_info(api_key)
425
+ uri = URI("#{@registry_url}/api_key.json")
426
+
427
+ request = Net::HTTP::Get.new(uri)
428
+ request["Authorization"] = api_key
429
+
430
+ response = send_request(uri, request)
431
+
432
+ case response
433
+ when Net::HTTPSuccess
434
+ JSON.parse(response.body)
435
+ else
436
+ nil
437
+ end
438
+ rescue StandardError
439
+ nil
440
+ end
441
+
442
+ private
443
+
444
+ def fetch_json(uri)
445
+ cached = @http_cache[uri.to_s]
446
+ if cached && Time.now - cached[:time] < 300 # 5 min cache
447
+ return cached[:data]
448
+ end
449
+
450
+ response = Net::HTTP.get_response(uri)
451
+ return nil unless response.is_a?(Net::HTTPSuccess)
452
+
453
+ data = JSON.parse(response.body)
454
+ @http_cache[uri.to_s] = { data: data, time: Time.now }
455
+ data
456
+ rescue JSON::ParserError
457
+ nil
458
+ end
459
+
460
+ def download_file(uri, path)
461
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
462
+ request = Net::HTTP::Get.new(uri)
463
+ http.request(request) do |response|
464
+ File.open(path, "wb") do |file|
465
+ response.read_body { |chunk| file.write(chunk) }
466
+ end
467
+ end
468
+ end
469
+ end
470
+
471
+ def send_request(uri, request)
472
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
473
+ http.request(request)
474
+ end
475
+ end
476
+
477
+ def parse_dependencies(deps)
478
+ return {} unless deps
479
+
480
+ result = {}
481
+ (deps["runtime"] || []).each do |dep|
482
+ result[dep["name"]] = dep["requirements"]
483
+ end
484
+ result
485
+ end
486
+
487
+ def extract_types(gem_path, target_dir)
488
+ # In a real implementation, would extract .d.trb files from gem
489
+ # For now, just create marker
490
+ File.write(File.join(target_dir, ".extracted"), Time.now.iso8601)
491
+ end
492
+ end
493
+
494
+ # Package registry (local or remote)
495
+ class PackageRegistry
496
+ attr_reader :packages, :local_path, :remote
497
+
498
+ def initialize(local_path: nil, remote_url: nil)
499
+ @local_path = local_path || ".trb-packages"
500
+ @remote_url = remote_url
501
+ @packages = {}
502
+ @remote = RemoteRegistry.new(registry_url: remote_url) if remote_url
503
+ FileUtils.mkdir_p(@local_path) if @local_path
504
+ end
505
+
506
+ # Register a package
507
+ def register(manifest)
508
+ @packages[manifest.name] ||= {}
509
+ @packages[manifest.name][manifest.version] = {
510
+ dependencies: manifest.dependencies,
511
+ types: manifest.types
512
+ }
513
+ end
514
+
515
+ # Get available versions
516
+ def get_versions(name)
517
+ @packages[name]&.keys || []
518
+ end
519
+
520
+ # Get specific package info
521
+ def get_package(name, version)
522
+ return nil unless @packages[name]
523
+
524
+ if version == "*"
525
+ latest = get_versions(name).map { |v| SemanticVersion.parse(v) }.compact.max
526
+ return nil unless latest
527
+ version = latest.to_s
528
+ end
529
+
530
+ @packages[name][version]
531
+ end
532
+
533
+ # Load package from local directory
534
+ def load_local(package_dir)
535
+ manifest_path = File.join(package_dir, PackageManifest::MANIFEST_FILE)
536
+ return nil unless File.exist?(manifest_path)
537
+
538
+ manifest = PackageManifest.load(manifest_path)
539
+ register(manifest) if manifest&.valid?
540
+ manifest
541
+ end
542
+
543
+ # Install package to local cache
544
+ def install(name, version, target_dir = nil)
545
+ target = target_dir || File.join(@local_path, name, version)
546
+ FileUtils.mkdir_p(target)
547
+
548
+ pkg = get_package(name, version)
549
+ return nil unless pkg
550
+
551
+ # Copy type definitions
552
+ types_pattern = pkg[:types] || "**/*.d.trb"
553
+ # In real implementation, would download from registry
554
+
555
+ { name: name, version: version, path: target }
556
+ end
557
+
558
+ # Search packages by keyword
559
+ def search(keyword)
560
+ @packages.select do |name, versions|
561
+ name.include?(keyword) ||
562
+ versions.values.any? { |v| v[:keywords]&.include?(keyword) }
563
+ end.keys
564
+ end
565
+ end
566
+
567
+ # Package manager main class
568
+ class PackageManager
569
+ attr_reader :manifest, :registry, :resolver
570
+
571
+ def initialize(project_dir: ".")
572
+ @project_dir = project_dir
573
+ @manifest = PackageManifest.load(File.join(project_dir, PackageManifest::MANIFEST_FILE))
574
+ @registry = PackageRegistry.new(local_path: File.join(project_dir, ".trb-packages"))
575
+ @resolver = DependencyResolver.new(@registry)
576
+ end
577
+
578
+ # Initialize a new package
579
+ def init(name: nil)
580
+ @manifest = PackageManifest.new(
581
+ name: name || File.basename(@project_dir),
582
+ version: "0.1.0",
583
+ types: "lib/types/**/*.d.trb"
584
+ )
585
+ @manifest.save(File.join(@project_dir, PackageManifest::MANIFEST_FILE))
586
+ @manifest
587
+ end
588
+
589
+ # Add a dependency
590
+ def add(name, version = "*", dev: false)
591
+ ensure_manifest!
592
+
593
+ if dev
594
+ @manifest.add_dev_dependency(name, version)
595
+ else
596
+ @manifest.add_dependency(name, version)
597
+ end
598
+
599
+ @manifest.save(File.join(@project_dir, PackageManifest::MANIFEST_FILE))
600
+
601
+ # Resolve and install
602
+ install
603
+ end
604
+
605
+ # Remove a dependency
606
+ def remove(name)
607
+ ensure_manifest!
608
+ @manifest.remove_dependency(name)
609
+ @manifest.save(File.join(@project_dir, PackageManifest::MANIFEST_FILE))
610
+ end
611
+
612
+ # Install all dependencies
613
+ def install
614
+ ensure_manifest!
615
+
616
+ result = @resolver.resolve(@manifest)
617
+
618
+ if result[:conflicts].any?
619
+ raise "Dependency conflicts: #{result[:conflicts].join(', ')}"
620
+ end
621
+
622
+ installed = []
623
+ result[:resolved].each do |name, version|
624
+ pkg = @registry.install(name, version)
625
+ installed << pkg if pkg
626
+ end
627
+
628
+ # Generate lockfile
629
+ generate_lockfile(result[:resolved])
630
+
631
+ installed
632
+ end
633
+
634
+ # Update dependencies
635
+ def update(name = nil)
636
+ ensure_manifest!
637
+
638
+ if name
639
+ # Update specific package
640
+ current = @manifest.dependencies[name]
641
+ if current
642
+ @manifest.dependencies[name] = "*" # Get latest
643
+ result = @resolver.resolve(@manifest)
644
+ if result[:resolved][name]
645
+ @manifest.dependencies[name] = "^#{result[:resolved][name]}"
646
+ @manifest.save(File.join(@project_dir, PackageManifest::MANIFEST_FILE))
647
+ end
648
+ end
649
+ else
650
+ # Update all
651
+ @manifest.dependencies.each_key do |dep_name|
652
+ update(dep_name)
653
+ end
654
+ end
655
+
656
+ install
657
+ end
658
+
659
+ # List installed packages
660
+ def list
661
+ lockfile_path = File.join(@project_dir, ".trb-lock.json")
662
+ return {} unless File.exist?(lockfile_path)
663
+
664
+ JSON.parse(File.read(lockfile_path))
665
+ rescue JSON::ParserError
666
+ {}
667
+ end
668
+
669
+ # Publish package (stub - would integrate with real registry)
670
+ def publish
671
+ ensure_manifest!
672
+
673
+ unless @manifest.valid?
674
+ raise "Invalid manifest: missing name or version"
675
+ end
676
+
677
+ # Validate package
678
+ validate_package
679
+
680
+ # In real implementation, would upload to registry
681
+ {
682
+ name: @manifest.name,
683
+ version: @manifest.version,
684
+ status: :published
685
+ }
686
+ end
687
+
688
+ # Create deprecation notice
689
+ def deprecate(version, message)
690
+ ensure_manifest!
691
+
692
+ {
693
+ package: @manifest.name,
694
+ version: version,
695
+ deprecated: true,
696
+ message: message
697
+ }
698
+ end
699
+
700
+ private
701
+
702
+ def ensure_manifest!
703
+ unless @manifest
704
+ raise "No manifest found. Run 'init' first."
705
+ end
706
+ end
707
+
708
+ def generate_lockfile(resolved)
709
+ lockfile = {
710
+ lockfileVersion: 1,
711
+ packages: resolved,
712
+ generatedAt: Time.now.iso8601
713
+ }
714
+
715
+ File.write(
716
+ File.join(@project_dir, ".trb-lock.json"),
717
+ JSON.pretty_generate(lockfile)
718
+ )
719
+ end
720
+
721
+ def validate_package
722
+ errors = []
723
+
724
+ errors << "Missing package name" unless @manifest.name
725
+ errors << "Invalid version" unless SemanticVersion.parse(@manifest.version)
726
+
727
+ # Check for type files
728
+ types_pattern = @manifest.types || "**/*.d.trb"
729
+ types_files = Dir.glob(File.join(@project_dir, types_pattern))
730
+ errors << "No type definition files found" if types_files.empty?
731
+
732
+ raise errors.join(", ") unless errors.empty?
733
+ end
734
+ end
735
+ end