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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +221 -0
- data/bin/trc +6 -0
- data/lib/t_ruby/benchmark.rb +592 -0
- data/lib/t_ruby/bundler_integration.rb +569 -0
- data/lib/t_ruby/cache.rb +774 -0
- data/lib/t_ruby/cli.rb +106 -0
- data/lib/t_ruby/compiler.rb +299 -0
- data/lib/t_ruby/config.rb +53 -0
- data/lib/t_ruby/constraint_checker.rb +441 -0
- data/lib/t_ruby/declaration_generator.rb +298 -0
- data/lib/t_ruby/doc_generator.rb +474 -0
- data/lib/t_ruby/error_handler.rb +132 -0
- data/lib/t_ruby/generic_type_parser.rb +68 -0
- data/lib/t_ruby/intersection_type_parser.rb +30 -0
- data/lib/t_ruby/ir.rb +1301 -0
- data/lib/t_ruby/lsp_server.rb +994 -0
- data/lib/t_ruby/package_manager.rb +735 -0
- data/lib/t_ruby/parser.rb +245 -0
- data/lib/t_ruby/parser_combinator.rb +942 -0
- data/lib/t_ruby/rbs_generator.rb +71 -0
- data/lib/t_ruby/runtime_validator.rb +367 -0
- data/lib/t_ruby/smt_solver.rb +1076 -0
- data/lib/t_ruby/type_alias_registry.rb +102 -0
- data/lib/t_ruby/type_checker.rb +770 -0
- data/lib/t_ruby/type_erasure.rb +26 -0
- data/lib/t_ruby/type_inferencer.rb +580 -0
- data/lib/t_ruby/union_type_parser.rb +38 -0
- data/lib/t_ruby/version.rb +5 -0
- data/lib/t_ruby/watcher.rb +320 -0
- data/lib/t_ruby.rb +42 -0
- metadata +87 -0
|
@@ -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
|