t-ruby 0.0.34 → 0.0.36

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aaa0445c84cb0a6685e788645a19fee5d30d2f1e6f79052122fb16e8a3f08e91
4
- data.tar.gz: 446845b5b1e470964f664d590197262f0e3555b82dd59f7b75a484324b99ec63
3
+ metadata.gz: f8847929796ade811f3b9c3cd3ddcb86448e18927e40c2318e562dc9c35e312b
4
+ data.tar.gz: 526a69c0dc752147ed3a29b208ebb85cd019a20b03efbd21ed28a25d5badc37b
5
5
  SHA512:
6
- metadata.gz: e422c5535edcb5085efd7018825c1675b06b3c2bb3657da1c6d9f6b07202233b53c03c145888fbdc221fc78df4873d18c2d0f4326d19ede16134eda409fb7418
7
- data.tar.gz: 0d988eaa6c08a586af4c965447087467e500cc75de4d7bc879c239c3f6924caf504123fc3117ed9be4e676b0ae2715bdadf888218e8072702c1a66bbb329a288
6
+ metadata.gz: 55d2c62ad5fd9071da3bfd363fad13e14d08f340c9ed8b19ab1c5b677db4a4e583dc8f183f67006efbfa44a2fbd1e0c77f14c6dd7af39b6c2d745978b938dcdf
7
+ data.tar.gz: 93d96b65f461f68dc3833a1be714b51a616edefc1b01e97d56016cddbba7a48843f098efa49634d3a55043c8ffca21bd47cad660200cd63fd75e5deb92a2811e
data/README.md CHANGED
@@ -138,7 +138,7 @@ We still love Ruby, and we want this to be
138
138
  gem install t-ruby
139
139
 
140
140
  # from source
141
- git clone https://github.com/pyhyun/t-ruby
141
+ git clone https://github.com/type-ruby/t-ruby
142
142
  cd t-ruby && bundle install
143
143
  ```
144
144
 
@@ -152,10 +152,21 @@ trc --version
152
152
 
153
153
  ## Quick start
154
154
 
155
- ### 1. Write `.trb`
155
+ ### 1. Initialize project
156
+
157
+ ```bash
158
+ trc --init
159
+ ```
160
+
161
+ This creates:
162
+ - `trbconfig.yml` — project configuration
163
+ - `src/` — source directory
164
+ - `build/` — output directory
165
+
166
+ ### 2. Write `.trb`
156
167
 
157
168
  ```trb
158
- # hello.trb
169
+ # src/hello.trb
159
170
  def greet(name: String): String
160
171
  "Hello, #{name}!"
161
172
  end
@@ -163,19 +174,69 @@ end
163
174
  puts greet("world")
164
175
  ```
165
176
 
166
- ### 2. Compile
177
+ ### 3. Compile
167
178
 
168
179
  ```bash
169
- trc hello.trb
180
+ trc src/hello.trb
170
181
  ```
171
182
 
172
- ### 3. Run
183
+ ### 4. Run
173
184
 
174
185
  ```bash
175
186
  ruby build/hello.rb
176
187
  # => Hello, world!
177
188
  ```
178
189
 
190
+ ### 5. Watch mode
191
+
192
+ ```bash
193
+ trc -w # Watch directories from trbconfig.yml (default: src/)
194
+ trc -w lib/ # Watch specific directory
195
+ ```
196
+
197
+ Files are automatically recompiled on change.
198
+
199
+ ---
200
+
201
+ ## Configuration
202
+
203
+ `trc --init` generates a `trbconfig.yml` file with all available options:
204
+
205
+ ```yaml
206
+ # T-Ruby configuration file
207
+ # See: https://type-ruby.github.io/docs/getting-started/project-configuration
208
+
209
+ source:
210
+ include:
211
+ - src
212
+ exclude: []
213
+ extensions:
214
+ - ".trb"
215
+ - ".rb"
216
+
217
+ output:
218
+ ruby_dir: build
219
+ # rbs_dir: sig # Optional: separate directory for .rbs files
220
+ preserve_structure: true
221
+ # clean_before_build: false
222
+
223
+ compiler:
224
+ strictness: standard # strict | standard | permissive
225
+ generate_rbs: true
226
+ target_ruby: "3.0"
227
+ # experimental: []
228
+ # checks:
229
+ # no_implicit_any: false
230
+ # no_unused_vars: false
231
+ # strict_nil: false
232
+
233
+ watch:
234
+ # paths: [] # Additional paths to watch
235
+ debounce: 100
236
+ # clear_screen: false
237
+ # on_success: "bundle exec rspec"
238
+ ```
239
+
179
240
  ---
180
241
 
181
242
  ## Features
@@ -191,10 +252,11 @@ ruby build/hello.rb
191
252
 
192
253
  ---
193
254
 
194
- ## Quick links
255
+ ## Links
195
256
 
196
- **Getting Started**
197
- - [VS Code Extension](./docs/vscode/en/getting-started.md)
257
+ **IDE Support**
258
+ - [VS Code Extension (and Cursor)](./docs/vscode/en/getting-started.md)
259
+ - [JetBrains Plugin](./docs/jetbrains/en/getting-started.md)
198
260
  - [Vim Setup](./docs/vim/en/getting-started.md)
199
261
  - [Neovim Setup](./docs/neovim/en/getting-started.md)
200
262
 
@@ -60,11 +60,11 @@ module TRuby
60
60
  # Export results to JSON
61
61
  def export_json(path = "benchmark_results.json")
62
62
  File.write(path, JSON.pretty_generate({
63
- timestamp: Time.now.iso8601,
64
- ruby_version: RUBY_VERSION,
65
- platform: RUBY_PLATFORM,
66
- results: @results
67
- }))
63
+ timestamp: Time.now.iso8601,
64
+ ruby_version: RUBY_VERSION,
65
+ platform: RUBY_PLATFORM,
66
+ results: @results,
67
+ }))
68
68
  end
69
69
 
70
70
  # Export results to Markdown
@@ -86,7 +86,7 @@ module TRuby
86
86
  benchmarks.each do |name, data|
87
87
  time_ms = (data[:avg_time] * 1000).round(2)
88
88
  memory_kb = (data[:memory] || 0).round(2)
89
- ips = data[:avg_time] > 0 ? (1.0 / data[:avg_time]).round(2) : 0
89
+ ips = data[:avg_time].positive? ? (1.0 / data[:avg_time]).round(2) : 0
90
90
  md << "| #{name} | #{time_ms} | #{memory_kb} | #{ips} |"
91
91
  end
92
92
  md << ""
@@ -116,7 +116,7 @@ module TRuby
116
116
  current: data[:avg_time],
117
117
  previous: prev_data[:avg_time],
118
118
  diff_percent: diff,
119
- improved: diff < 0
119
+ improved: diff.negative?,
120
120
  }
121
121
  end
122
122
  end
@@ -255,7 +255,7 @@ module TRuby
255
255
  print_result(:incremental_single, results[:incremental_single])
256
256
 
257
257
  # Calculate speedup
258
- if results[:full_compile][:avg_time] > 0
258
+ if results[:full_compile][:avg_time].positive?
259
259
  speedup = results[:full_compile][:avg_time] / results[:incremental_single][:avg_time]
260
260
  puts " Incremental speedup: #{speedup.round(2)}x"
261
261
  end
@@ -317,7 +317,7 @@ module TRuby
317
317
  print_result(:parallel_4, results[:parallel_4])
318
318
 
319
319
  # Print speedups
320
- if results[:sequential][:avg_time] > 0
320
+ if results[:sequential][:avg_time].positive?
321
321
  puts " Parallel(2) speedup: #{(results[:sequential][:avg_time] / results[:parallel_2][:avg_time]).round(2)}x"
322
322
  puts " Parallel(4) speedup: #{(results[:sequential][:avg_time] / results[:parallel_4][:avg_time]).round(2)}x"
323
323
  end
@@ -332,7 +332,7 @@ module TRuby
332
332
 
333
333
  # Baseline memory
334
334
  GC.start
335
- baseline = get_memory_usage
335
+ get_memory_usage
336
336
 
337
337
  # Parser memory
338
338
  content = generate_test_content(0)
@@ -374,20 +374,20 @@ module TRuby
374
374
  small_file: generate_test_content(0, lines: 10),
375
375
  medium_file: generate_test_content(0, lines: 100),
376
376
  large_file: generate_test_content(0, lines: 500),
377
- complex_types: generate_complex_types_content
377
+ complex_types: generate_complex_types_content,
378
378
  }
379
379
  when :type_checking
380
380
  {
381
381
  simple_types: generate_simple_types_content,
382
382
  generic_types: generate_generic_types_content,
383
383
  union_types: generate_union_types_content,
384
- interface_types: generate_interface_types_content
384
+ interface_types: generate_interface_types_content,
385
385
  }
386
386
  when :compilation
387
387
  {
388
388
  minimal: "def hello: void; end",
389
389
  with_types: generate_test_content(0, lines: 50),
390
- with_interfaces: generate_interface_types_content
390
+ with_interfaces: generate_interface_types_content,
391
391
  }
392
392
  else
393
393
  {}
@@ -396,7 +396,7 @@ module TRuby
396
396
 
397
397
  def generate_test_content(seed, lines: 50, modified: false)
398
398
  content = []
399
- content << "# Test file #{seed}#{modified ? ' (modified)' : ''}"
399
+ content << "# Test file #{seed}#{" (modified)" if modified}"
400
400
  content << ""
401
401
  content << "type CustomType#{seed} = String | Integer | nil"
402
402
  content << ""
@@ -522,7 +522,7 @@ module TRuby
522
522
  min_time: min,
523
523
  max_time: max,
524
524
  std_dev: std_dev,
525
- iterations: times.length
525
+ iterations: times.length,
526
526
  }
527
527
  end
528
528
 
@@ -571,7 +571,7 @@ module TRuby
571
571
  iterations.times do
572
572
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
573
573
  yield
574
- times << Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
574
+ times << (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
575
575
  end
576
576
 
577
577
  avg = times.sum / times.length
@@ -42,7 +42,7 @@ module TRuby
42
42
  installed_gems = parse_gemfile_lock
43
43
  type_packages = {}
44
44
 
45
- installed_gems.each do |gem_name, version|
45
+ installed_gems.each_key do |gem_name|
46
46
  type_gem = find_type_gem(gem_name)
47
47
  type_packages[gem_name] = type_gem if type_gem
48
48
  end
@@ -93,7 +93,7 @@ module TRuby
93
93
  types_group: TYPES_GROUP.to_s,
94
94
  type_gems: list_type_gems,
95
95
  local_types: list_local_types,
96
- generated_at: Time.now.iso8601
96
+ generated_at: Time.now.iso8601,
97
97
  }
98
98
 
99
99
  manifest_path = File.join(@project_dir, ".trb-bundle.json")
@@ -128,15 +128,15 @@ module TRuby
128
128
 
129
129
  next unless base_version && type_version
130
130
 
131
- unless versions_compatible?(base_version, type_version)
132
- issues << {
133
- gem: base_gem,
134
- gem_version: base_version,
135
- type_gem: type_info[:name],
136
- type_version: type_version,
137
- message: "Version mismatch: #{base_gem}@#{base_version} vs #{type_info[:name]}@#{type_version}"
138
- }
139
- end
131
+ next if versions_compatible?(base_version, type_version)
132
+
133
+ issues << {
134
+ gem: base_gem,
135
+ gem_version: base_version,
136
+ type_gem: type_info[:name],
137
+ type_version: type_version,
138
+ message: "Version mismatch: #{base_gem}@#{base_version} vs #{type_info[:name]}@#{type_version}",
139
+ }
140
140
  end
141
141
 
142
142
  issues
@@ -201,21 +201,21 @@ module TRuby
201
201
 
202
202
  # Create a sample .d.trb file
203
203
  sample_path = File.join(types_dir, "custom.d.trb")
204
- unless File.exist?(sample_path)
205
- File.write(sample_path, <<~TRB)
206
- # Custom type definitions for your project
207
- # These types are available throughout your T-Ruby code
208
-
209
- # Example type alias
210
- # type UserId = String
211
-
212
- # Example interface
213
- # interface Serializable
214
- # to_json: String
215
- # from_json: (String) -> self
216
- # end
217
- TRB
218
- end
204
+ return if File.exist?(sample_path)
205
+
206
+ File.write(sample_path, <<~TRB)
207
+ # Custom type definitions for your project
208
+ # These types are available throughout your T-Ruby code
209
+
210
+ # Example type alias
211
+ # type UserId = String
212
+
213
+ # Example interface
214
+ # interface Serializable
215
+ # to_json: String
216
+ # from_json: (String) -> self
217
+ # end
218
+ TRB
219
219
  end
220
220
 
221
221
  def append_to_gemfile(gem_name, version, group:)
@@ -276,7 +276,7 @@ module TRuby
276
276
  # This is a simplified check - in production would query RubyGems API
277
277
  {
278
278
  name: type_gem_name,
279
- available: check_gem_availability(type_gem_name)
279
+ available: check_gem_availability(type_gem_name),
280
280
  }
281
281
  end
282
282
 
@@ -305,7 +305,7 @@ module TRuby
305
305
  name: name,
306
306
  base_gem: base_gem,
307
307
  version: version,
308
- path: find_gem_path(name, version)
308
+ path: find_gem_path(name, version),
309
309
  }
310
310
  end
311
311
  end
@@ -315,7 +315,7 @@ module TRuby
315
315
  possible_paths = [
316
316
  File.join(ENV["GEM_HOME"] || "", "gems", "#{gem_name}-#{version}"),
317
317
  File.join(Dir.home, ".gem", "ruby", "*", "gems", "#{gem_name}-#{version}"),
318
- File.join(@project_dir, "vendor", "bundle", "**", "gems", "#{gem_name}-#{version}")
318
+ File.join(@project_dir, "vendor", "bundle", "**", "gems", "#{gem_name}-#{version}"),
319
319
  ]
320
320
 
321
321
  possible_paths.each do |pattern|
@@ -398,7 +398,7 @@ module TRuby
398
398
  {
399
399
  name: gem_info[:name],
400
400
  base_gem: gem_info[:base_gem],
401
- version: gem_info[:version]
401
+ version: gem_info[:version],
402
402
  }
403
403
  end
404
404
  end
@@ -553,11 +553,9 @@ module TRuby
553
553
  migrated = []
554
554
 
555
555
  # Read existing T-Ruby manifest
556
- if @manifest
557
- @manifest.dependencies.each do |name, version|
558
- result = @bundler.add_type_gem(name, version: version)
559
- migrated << result
560
- end
556
+ @manifest&.dependencies&.each do |name, version|
557
+ result = @bundler.add_type_gem(name, version: version)
558
+ migrated << result
561
559
  end
562
560
 
563
561
  # Generate new bundle manifest
data/lib/t_ruby/cache.rb CHANGED
@@ -32,7 +32,7 @@ module TRuby
32
32
  key: @key,
33
33
  value: @value,
34
34
  created_at: @created_at.to_i,
35
- hits: @hits
35
+ hits: @hits,
36
36
  }
37
37
  end
38
38
  end
@@ -96,6 +96,7 @@ module TRuby
96
96
  def hit_rate
97
97
  total = @hits + @misses
98
98
  return 0.0 if total.zero?
99
+
99
100
  @hits.to_f / total
100
101
  end
101
102
 
@@ -105,7 +106,7 @@ module TRuby
105
106
  max_size: @max_size,
106
107
  hits: @hits,
107
108
  misses: @misses,
108
- hit_rate: hit_rate
109
+ hit_rate: hit_rate,
109
110
  }
110
111
  end
111
112
 
@@ -160,7 +161,7 @@ module TRuby
160
161
 
161
162
  def delete(key)
162
163
  path = cache_path(key)
163
- File.delete(path) if File.exist?(path)
164
+ FileUtils.rm_f(path)
164
165
  end
165
166
 
166
167
  def clear
@@ -260,7 +261,7 @@ module TRuby
260
261
  # Declaration file cache
261
262
  class DeclarationCache
262
263
  def initialize(cache_dir: ".t-ruby-cache/declarations")
263
- @file_cache = FileCache.new(cache_dir: cache_dir, max_age: 86400) # 24 hours
264
+ @file_cache = FileCache.new(cache_dir: cache_dir, max_age: 86_400) # 24 hours
264
265
  @memory_cache = MemoryCache.new(max_size: 200)
265
266
  end
266
267
 
@@ -368,6 +369,7 @@ module TRuby
368
369
 
369
370
  def file_hash(file_path)
370
371
  return nil unless File.exist?(file_path)
372
+
371
373
  Digest::SHA256.hexdigest(File.read(file_path))
372
374
  end
373
375
  end
@@ -412,7 +414,11 @@ module TRuby
412
414
  threads = @thread_count.times.map do
413
415
  Thread.new do
414
416
  loop do
415
- file = queue.pop(true) rescue break
417
+ file = begin
418
+ queue.pop(true)
419
+ rescue StandardError
420
+ break
421
+ end
416
422
  result = block.call(file)
417
423
  mutex.synchronize { results << result }
418
424
  end
@@ -428,7 +434,7 @@ module TRuby
428
434
  def determine_thread_count
429
435
  # Use number of CPU cores, max 8
430
436
  [Etc.nprocessors, 8].min
431
- rescue
437
+ rescue StandardError
432
438
  4
433
439
  end
434
440
 
@@ -443,8 +449,8 @@ module TRuby
443
449
 
444
450
  def initialize(type_checker: nil)
445
451
  @type_checker = type_checker || TypeChecker.new
446
- @file_types = {} # file_path => { types: [], functions: [], interfaces: [] }
447
- @global_registry = {} # name => { file: path, kind: :type/:func/:interface, definition: ... }
452
+ @file_types = {} # file_path => { types: [], functions: [], interfaces: [] }
453
+ @global_registry = {} # name => { file: path, kind: :type/:func/:interface, definition: ... }
448
454
  @errors = []
449
455
  @warnings = []
450
456
  end
@@ -489,7 +495,7 @@ module TRuby
489
495
  {
490
496
  success: @errors.empty?,
491
497
  errors: @errors,
492
- warnings: @warnings
498
+ warnings: @warnings,
493
499
  }
494
500
  end
495
501
 
@@ -502,24 +508,20 @@ module TRuby
502
508
  when IR::MethodDef
503
509
  # Check parameter types
504
510
  decl.params.each do |param|
505
- if param.type_annotation
506
- unless type_exists?(param.type_annotation)
507
- file_errors << {
508
- file: file_path,
509
- message: "Unknown type '#{type_name(param.type_annotation)}' in parameter '#{param.name}'"
510
- }
511
- end
512
- end
511
+ next unless param.type_annotation && !type_exists?(param.type_annotation)
512
+
513
+ file_errors << {
514
+ file: file_path,
515
+ message: "Unknown type '#{type_name(param.type_annotation)}' in parameter '#{param.name}'",
516
+ }
513
517
  end
514
518
 
515
519
  # Check return type
516
- if decl.return_type
517
- unless type_exists?(decl.return_type)
518
- file_errors << {
519
- file: file_path,
520
- message: "Unknown return type '#{type_name(decl.return_type)}' in function '#{decl.name}'"
521
- }
522
- end
520
+ if decl.return_type && !type_exists?(decl.return_type)
521
+ file_errors << {
522
+ file: file_path,
523
+ message: "Unknown return type '#{type_name(decl.return_type)}' in function '#{decl.name}'",
524
+ }
523
525
  end
524
526
  end
525
527
  end
@@ -552,7 +554,7 @@ module TRuby
552
554
  # Duplicate definition from different file
553
555
  @warnings << {
554
556
  message: "#{kind.to_s.capitalize} '#{name}' defined in multiple files",
555
- files: [@global_registry[name][:file], file_path]
557
+ files: [@global_registry[name][:file], file_path],
556
558
  }
557
559
  end
558
560
 
@@ -568,7 +570,7 @@ module TRuby
568
570
  duplicates.each do |name|
569
571
  @errors << {
570
572
  file: file,
571
- message: "Duplicate definition of '#{name}'"
573
+ message: "Duplicate definition of '#{name}'",
572
574
  }
573
575
  end
574
576
  end
@@ -580,12 +582,12 @@ module TRuby
580
582
  info[:types].each do |type_info|
581
583
  referenced_types = extract_type_references(type_info[:definition])
582
584
  referenced_types.each do |ref|
583
- unless type_exists_by_name?(ref)
584
- @errors << {
585
- file: file_path,
586
- message: "Unresolved type reference '#{ref}' in type alias '#{type_info[:name]}'"
587
- }
588
- end
585
+ next if type_exists_by_name?(ref)
586
+
587
+ @errors << {
588
+ file: file_path,
589
+ message: "Unresolved type reference '#{ref}' in type alias '#{type_info[:name]}'",
590
+ }
589
591
  end
590
592
  end
591
593
  end
@@ -608,13 +610,15 @@ module TRuby
608
610
  when IR::NullableType
609
611
  type_exists?(type_node.inner_type)
610
612
  else
611
- true # Assume valid for unknown types
613
+ true # Assume valid for unknown types
612
614
  end
613
615
  end
614
616
 
615
617
  def type_exists_by_name?(name)
616
- return true if %w[String Integer Float Boolean Array Hash Symbol void nil Object Numeric Enumerable].include?(name)
618
+ return true if %w[String Integer Float Boolean Array Hash Symbol void nil Object Numeric
619
+ Enumerable].include?(name)
617
620
  return true if @global_registry[name]
621
+
618
622
  false
619
623
  end
620
624
 
@@ -655,7 +659,7 @@ module TRuby
655
659
 
656
660
  def initialize(compiler, cache: nil, enable_cross_file: true)
657
661
  super(compiler, cache: cache)
658
- @ir_cache = {} # file_path => IR::Program
662
+ @ir_cache = {} # file_path => IR::Program
659
663
  @cross_file_checker = CrossFileTypeChecker.new if enable_cross_file
660
664
  end
661
665
 
@@ -685,11 +689,9 @@ module TRuby
685
689
 
686
690
  # First pass: compile and register all files
687
691
  file_paths.each do |file_path|
688
- begin
689
- results[file_path] = compile_with_ir(file_path)
690
- rescue => e
691
- errors << { file: file_path, error: e.message }
692
- end
692
+ results[file_path] = compile_with_ir(file_path)
693
+ rescue StandardError => e
694
+ errors << { file: file_path, error: e.message }
693
695
  end
694
696
 
695
697
  # Second pass: cross-file type checking
@@ -701,7 +703,7 @@ module TRuby
701
703
  {
702
704
  results: results,
703
705
  errors: errors,
704
- success: errors.empty?
706
+ success: errors.empty?,
705
707
  }
706
708
  end
707
709
 
@@ -721,6 +723,7 @@ module TRuby
721
723
 
722
724
  def file_hash(file_path)
723
725
  return nil unless File.exist?(file_path)
726
+
724
727
  Digest::SHA256.hexdigest(File.read(file_path))
725
728
  end
726
729
  end
@@ -751,7 +754,7 @@ module TRuby
751
754
  @timings.sort_by { |_, v| -v }.each do |name, time|
752
755
  calls = @call_counts[name]
753
756
  avg = time / calls
754
- puts "#{name}: #{format('%.3f', time)}s total, #{calls} calls, #{format('%.3f', avg * 1000)}ms avg"
757
+ puts "#{name}: #{format("%.3f", time)}s total, #{calls} calls, #{format("%.3f", avg * 1000)}ms avg"
755
758
  end
756
759
  end
757
760
 
@@ -766,7 +769,7 @@ module TRuby
766
769
  name: name,
767
770
  total_time: time,
768
771
  call_count: @call_counts[name],
769
- avg_time: time / @call_counts[name]
772
+ avg_time: time / @call_counts[name],
770
773
  }
771
774
  end
772
775
  end
data/lib/t_ruby/cli.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module TRuby
4
4
  class CLI
5
- HELP_TEXT = <<~HELP
5
+ HELP_TEXT = <<~HELP.freeze
6
6
  t-ruby compiler (trc) v#{VERSION}
7
7
 
8
8
  Usage:
@@ -145,10 +145,10 @@ module TRuby
145
145
 
146
146
  # Output results
147
147
  if created.any?
148
- puts "Created: #{created.join(', ')}"
148
+ puts "Created: #{created.join(", ")}"
149
149
  end
150
150
  if skipped.any?
151
- puts "Skipped (already exists): #{skipped.join(', ')}"
151
+ puts "Skipped (already exists): #{skipped.join(", ")}"
152
152
  end
153
153
  if created.empty? && skipped.any?
154
154
  puts "Project already initialized."