makit 0.0.98 → 0.0.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -0
  3. data/exe/makit +5 -0
  4. data/lib/makit/apache.rb +7 -11
  5. data/lib/makit/cli/build_commands.rb +500 -0
  6. data/lib/makit/cli/generators/base_generator.rb +74 -0
  7. data/lib/makit/cli/generators/dotnet_generator.rb +50 -0
  8. data/lib/makit/cli/generators/generator_factory.rb +49 -0
  9. data/lib/makit/cli/generators/node_generator.rb +50 -0
  10. data/lib/makit/cli/generators/ruby_generator.rb +77 -0
  11. data/lib/makit/cli/generators/rust_generator.rb +50 -0
  12. data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -0
  13. data/lib/makit/cli/generators/templates/node_templates.rb +161 -0
  14. data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -0
  15. data/lib/makit/cli/generators/templates/ruby/gemspec.rb +40 -0
  16. data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -0
  17. data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -0
  18. data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -0
  19. data/lib/makit/cli/generators/templates/ruby/test.rb +39 -0
  20. data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -0
  21. data/lib/makit/cli/generators/templates/ruby/version.rb +29 -0
  22. data/lib/makit/cli/generators/templates/rust_templates.rb +128 -0
  23. data/lib/makit/cli/main.rb +48 -19
  24. data/lib/makit/cli/project_commands.rb +868 -0
  25. data/lib/makit/cli/repository_commands.rb +661 -0
  26. data/lib/makit/cli/utility_commands.rb +521 -0
  27. data/lib/makit/command_runner.rb +187 -128
  28. data/lib/makit/commands/compatibility.rb +365 -0
  29. data/lib/makit/commands/factory.rb +359 -0
  30. data/lib/makit/commands/middleware/base.rb +73 -0
  31. data/lib/makit/commands/middleware/cache.rb +248 -0
  32. data/lib/makit/commands/middleware/command_logger.rb +323 -0
  33. data/lib/makit/commands/middleware/unified_logger.rb +243 -0
  34. data/lib/makit/commands/middleware/validator.rb +269 -0
  35. data/lib/makit/commands/request.rb +254 -0
  36. data/lib/makit/commands/result.rb +323 -0
  37. data/lib/makit/commands/runner.rb +317 -0
  38. data/lib/makit/commands/strategies/base.rb +160 -0
  39. data/lib/makit/commands/strategies/synchronous.rb +134 -0
  40. data/lib/makit/commands.rb +24 -3
  41. data/lib/makit/configuration/gitlab_helper.rb +60 -0
  42. data/lib/makit/configuration/project.rb +127 -0
  43. data/lib/makit/configuration/rakefile_helper.rb +43 -0
  44. data/lib/makit/configuration/step.rb +34 -0
  45. data/lib/makit/configuration.rb +14 -0
  46. data/lib/makit/content/default_gitignore.rb +4 -2
  47. data/lib/makit/content/default_rakefile.rb +4 -2
  48. data/lib/makit/content/gem_rakefile.rb +4 -2
  49. data/lib/makit/context.rb +1 -0
  50. data/lib/makit/data.rb +9 -10
  51. data/lib/makit/directories.rb +48 -52
  52. data/lib/makit/directory.rb +38 -52
  53. data/lib/makit/docs/files.rb +5 -10
  54. data/lib/makit/docs/rake.rb +16 -20
  55. data/lib/makit/dotnet/cli.rb +65 -0
  56. data/lib/makit/dotnet/project.rb +153 -0
  57. data/lib/makit/dotnet/solution.rb +38 -0
  58. data/lib/makit/dotnet/solution_classlib.rb +239 -0
  59. data/lib/makit/dotnet/solution_console.rb +264 -0
  60. data/lib/makit/dotnet/solution_maui.rb +354 -0
  61. data/lib/makit/dotnet/solution_wasm.rb +275 -0
  62. data/lib/makit/dotnet/solution_wpf.rb +304 -0
  63. data/lib/makit/dotnet.rb +54 -171
  64. data/lib/makit/email.rb +46 -17
  65. data/lib/makit/environment.rb +22 -19
  66. data/lib/makit/examples/runner.rb +370 -0
  67. data/lib/makit/exceptions.rb +45 -0
  68. data/lib/makit/fileinfo.rb +3 -5
  69. data/lib/makit/files.rb +12 -16
  70. data/lib/makit/gems.rb +40 -39
  71. data/lib/makit/git/cli.rb +54 -0
  72. data/lib/makit/git/repository.rb +90 -0
  73. data/lib/makit/git.rb +44 -91
  74. data/lib/makit/gitlab_runner.rb +0 -1
  75. data/lib/makit/humanize.rb +31 -23
  76. data/lib/makit/indexer.rb +15 -24
  77. data/lib/makit/logging/configuration.rb +305 -0
  78. data/lib/makit/logging/format_registry.rb +84 -0
  79. data/lib/makit/logging/formatters/base.rb +39 -0
  80. data/lib/makit/logging/formatters/console_formatter.rb +127 -0
  81. data/lib/makit/logging/formatters/json_formatter.rb +65 -0
  82. data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -0
  83. data/lib/makit/logging/formatters/text_formatter.rb +64 -0
  84. data/lib/makit/logging/log_request.rb +115 -0
  85. data/lib/makit/logging/logger.rb +159 -0
  86. data/lib/makit/logging/sinks/base.rb +91 -0
  87. data/lib/makit/logging/sinks/console.rb +72 -0
  88. data/lib/makit/logging/sinks/file_sink.rb +92 -0
  89. data/lib/makit/logging/sinks/structured.rb +129 -0
  90. data/lib/makit/logging/sinks/unified_file_sink.rb +303 -0
  91. data/lib/makit/logging.rb +452 -37
  92. data/lib/makit/markdown.rb +18 -18
  93. data/lib/makit/mp/basic_object_mp.rb +5 -4
  94. data/lib/makit/mp/command_mp.rb +5 -5
  95. data/lib/makit/mp/command_request.mp.rb +3 -2
  96. data/lib/makit/mp/project_mp.rb +85 -96
  97. data/lib/makit/mp/string_mp.rb +245 -73
  98. data/lib/makit/nuget.rb +27 -25
  99. data/lib/makit/port.rb +25 -27
  100. data/lib/makit/process.rb +127 -29
  101. data/lib/makit/protoc.rb +27 -24
  102. data/lib/makit/rake/cli.rb +196 -0
  103. data/lib/makit/rake.rb +6 -6
  104. data/lib/makit/ruby/cli.rb +185 -0
  105. data/lib/makit/ruby.rb +25 -0
  106. data/lib/makit/secrets.rb +18 -18
  107. data/lib/makit/serializer.rb +29 -27
  108. data/lib/makit/services/builder.rb +186 -0
  109. data/lib/makit/services/error_handler.rb +226 -0
  110. data/lib/makit/services/repository_manager.rb +229 -0
  111. data/lib/makit/services/validator.rb +112 -0
  112. data/lib/makit/setup/classlib.rb +53 -0
  113. data/lib/makit/setup/gem.rb +250 -0
  114. data/lib/makit/setup/runner.rb +40 -0
  115. data/lib/makit/show.rb +16 -16
  116. data/lib/makit/storage.rb +32 -37
  117. data/lib/makit/symbols.rb +12 -0
  118. data/lib/makit/task_hooks.rb +125 -0
  119. data/lib/makit/task_info.rb +63 -21
  120. data/lib/makit/tasks/at_exit.rb +13 -0
  121. data/lib/makit/tasks/build.rb +18 -0
  122. data/lib/makit/tasks/clean.rb +11 -0
  123. data/lib/makit/tasks/hook_manager.rb +239 -0
  124. data/lib/makit/tasks/init.rb +47 -0
  125. data/lib/makit/tasks/integrate.rb +15 -0
  126. data/lib/makit/tasks/pull_incoming.rb +12 -0
  127. data/lib/makit/tasks/setup.rb +6 -0
  128. data/lib/makit/tasks/sync.rb +11 -0
  129. data/lib/makit/tasks/task_monkey_patch.rb +79 -0
  130. data/lib/makit/tasks.rb +5 -150
  131. data/lib/makit/test_cache.rb +239 -0
  132. data/lib/makit/v1/makit.v1_pb.rb +34 -35
  133. data/lib/makit/v1/makit.v1_services_pb.rb +2 -0
  134. data/lib/makit/version.rb +1 -60
  135. data/lib/makit/wix.rb +23 -23
  136. data/lib/makit/yaml.rb +18 -6
  137. data/lib/makit.rb +2 -261
  138. metadata +109 -145
  139. data/lib/makit/cli/clean.rb +0 -14
  140. data/lib/makit/cli/clone.rb +0 -59
  141. data/lib/makit/cli/init.rb +0 -38
  142. data/lib/makit/cli/make.rb +0 -54
  143. data/lib/makit/cli/new.rb +0 -37
  144. data/lib/makit/cli/nuget_cache.rb +0 -38
  145. data/lib/makit/cli/pull.rb +0 -31
  146. data/lib/makit/cli/setup.rb +0 -71
  147. data/lib/makit/cli/work.rb +0 -21
  148. data/lib/makit/content/default_gitignore.txt +0 -222
@@ -0,0 +1,661 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "clamp"
4
+ require "json"
5
+ require "uri"
6
+ require "time"
7
+ require "find"
8
+
9
+ module Makit
10
+ module Cli
11
+ # Consolidated repository management commands
12
+ # Combines: add, clone, pull, list, remove, import functionality
13
+ class RepositoryCommand < Clamp::Command
14
+ self.description = <<~DESC
15
+ Manage git repositories tracked by Makit.
16
+
17
+ Available operations:
18
+ add - Add repository URL to tracking list
19
+ clone - Clone repository to local directory#{" "}
20
+ pull - Pull latest changes from repository
21
+ list - List tracked repositories
22
+ remove - Remove tracked repositories by pattern
23
+ import - Import repositories from filesystem
24
+ DESC
25
+ end
26
+
27
+ # Add repository to tracking list
28
+ class AddRepositoryCommand < Clamp::Command
29
+ self.description = <<~DESC
30
+ Add git repository URLs to the tracked list.
31
+
32
+ Examples:
33
+ makit repository add https://github.com/user/repo.git
34
+ makit repository add git@gitlab.com:group/project.git
35
+ DESC
36
+
37
+ parameter "GIT_URL", "The git repository URL to track", attribute_name: :git_url, required: true
38
+
39
+ def execute
40
+ validate_git_url(git_url)
41
+
42
+ if add_to_tracked_list(git_url)
43
+ puts "✅ Successfully added repository to tracked list: #{git_url}"
44
+ else
45
+ puts "ℹ️ Repository already tracked: #{git_url}"
46
+ end
47
+ rescue ArgumentError => e
48
+ warn "❌ Invalid repository URL: #{e.message}"
49
+ exit 1
50
+ rescue StandardError => e
51
+ warn "❌ Failed to add repository: #{e.message}"
52
+ exit 1
53
+ end
54
+
55
+ private
56
+
57
+ def validate_git_url(url)
58
+ raise ArgumentError, "URL cannot be empty" if url.nil? || url.strip.empty?
59
+
60
+ url = url.strip
61
+ valid_patterns = [
62
+ %r{^https?://\w+[\w.-]*\w+/[\w.-]+/[\w.-]+(?:\.git)?/?$}, # HTTPS
63
+ %r{^git@[\w.-]+:[\w.-]+/[\w.-]+(?:\.git)?$}, # SSH
64
+ %r{^ssh://git@[\w.-]+/[\w.-]+/[\w.-]+(?:\.git)?$}, # SSH with ssh://
65
+ %r{^[\w.-]+@[\w.-]+:[\w.-]+/[\w.-]+(?:\.git)?$}, # Generic SSH
66
+ ]
67
+
68
+ unless valid_patterns.any? { |pattern| url.match?(pattern) }
69
+ raise ArgumentError, "URL does not match expected git repository format"
70
+ end
71
+
72
+ return unless url.include?("github.com") || url.include?("gitlab.com") || url.include?("bitbucket.org")
73
+
74
+ validate_hosted_repository_url(url)
75
+ end
76
+
77
+ def validate_hosted_repository_url(url)
78
+ if url.start_with?("http")
79
+ path = URI.parse(url).path.sub(%r{^/}, "").sub(/\.git$/, "").sub(%r{/$}, "")
80
+ elsif url.include?("@")
81
+ path = url.split(":").last.sub(/\.git$/, "")
82
+ end
83
+
84
+ parts = path.split("/")
85
+ raise ArgumentError, "Repository path should be in format 'user/repository'" if parts.length < 2
86
+
87
+ parts.each do |part|
88
+ if part.empty? || part.match?(/[<>:"|?*]/)
89
+ raise ArgumentError, "Invalid characters in repository path: #{part}"
90
+ end
91
+ end
92
+ end
93
+
94
+ def add_to_tracked_list(url)
95
+ tracked_repos = load_tracked_repositories
96
+ return false if tracked_repos.any? { |repo| repo["url"] == url }
97
+
98
+ new_repo = {
99
+ "url" => url,
100
+ "added_at" => Time.now.iso8601,
101
+ "status" => "tracked",
102
+ }
103
+
104
+ tracked_repos << new_repo
105
+ save_tracked_repositories(tracked_repos)
106
+ true
107
+ end
108
+
109
+ def load_tracked_repositories
110
+ tracked_file = tracked_repositories_file
111
+ return [] unless File.exist?(tracked_file)
112
+
113
+ begin
114
+ JSON.parse(File.read(tracked_file))
115
+ rescue JSON::ParserError => e
116
+ warn "Warning: Could not parse tracked repositories file: #{e.message}"
117
+ []
118
+ end
119
+ end
120
+
121
+ def save_tracked_repositories(repositories)
122
+ tracked_file = tracked_repositories_file
123
+ FileUtils.mkdir_p(File.dirname(tracked_file))
124
+ sorted_repos = repositories.sort_by { |repo| repo["url"] }
125
+ File.write(tracked_file, JSON.pretty_generate(sorted_repos))
126
+ end
127
+
128
+ def tracked_repositories_file
129
+ File.join(Makit::Directories::ROOT, "tracked_repositories.json")
130
+ end
131
+ end
132
+
133
+ # Clone repository to local directory
134
+ class CloneRepositoryCommand < Clamp::Command
135
+ self.description = <<~DESC
136
+ Clone git repositories to local directories.
137
+
138
+ Examples:
139
+ makit repository clone https://github.com/user/repo
140
+ makit repository clone git@github.com:user/repo.git
141
+ DESC
142
+
143
+ parameter "GIT_REPOSITORY", "The git repository URL", attribute_name: :git_repository, required: true
144
+
145
+ def execute
146
+ clone_dir = Makit::Directories.get_clone_directory(git_repository)
147
+
148
+ if Dir.exist?(clone_dir)
149
+ puts "Repository already exists at: #{clone_dir}"
150
+ return
151
+ end
152
+
153
+ puts "Cloning repository: #{git_repository} to #{clone_dir}"
154
+
155
+ begin
156
+ commands = Makit.clone(git_repository)
157
+
158
+ if commands.any? && commands.last.exit_code != 0
159
+ handle_clone_error(commands.last, clone_dir)
160
+ elsif !Dir.exist?(clone_dir)
161
+ warn "Clone operation completed but directory not found: #{clone_dir}"
162
+ exit 1
163
+ else
164
+ puts "Successfully cloned repository to: #{clone_dir}"
165
+ end
166
+ rescue ArgumentError => e
167
+ warn "Invalid repository URL: #{e.message}"
168
+ exit 1
169
+ rescue StandardError => e
170
+ warn "Failed to clone repository: #{git_repository}"
171
+ puts e.message
172
+ exit 1
173
+ end
174
+ end
175
+
176
+ private
177
+
178
+ def handle_clone_error(clone_command, clone_dir)
179
+ summary = "Failed to clone repository: #{git_repository} to #{clone_dir}\n"
180
+ summary += "Please check the URL and your network connection.\n"
181
+ summary += "Exit code: #{clone_command.exit_code}\n"
182
+ summary += "Output: #{clone_command.output}" if clone_command.respond_to?(:output)
183
+ warn summary
184
+ exit 1
185
+ end
186
+ end
187
+
188
+ # Pull latest changes from repository
189
+ class PullRepositoryCommand < Clamp::Command
190
+ self.description = <<~DESC
191
+ Pull latest changes from git repositories.
192
+
193
+ Examples:
194
+ makit repository pull https://github.com/user/repo
195
+ DESC
196
+
197
+ parameter "GIT_REPOSITORY", "The git repository URL", attribute_name: :git_repository, required: true
198
+
199
+ def execute
200
+ commands = Makit.pull(git_repository)
201
+
202
+ if commands.any? && commands.last.exit_code.zero?
203
+ puts "Successfully pulled latest changes from: #{git_repository}"
204
+ else
205
+ warn "Failed to pull changes from: #{git_repository}"
206
+ exit 1
207
+ end
208
+ rescue ArgumentError => e
209
+ warn "Invalid repository URL: #{e.message}"
210
+ exit 1
211
+ rescue StandardError => e
212
+ warn "Failed to pull repository: #{git_repository}"
213
+ puts e.message
214
+ exit 1
215
+ end
216
+ end
217
+
218
+ # List tracked repositories
219
+ class ListRepositoryCommand < Clamp::Command
220
+ self.description = <<~DESC
221
+ List git repositories tracked by Makit.
222
+
223
+ Examples:
224
+ makit repository list
225
+ makit repository list --details
226
+ makit repository list --tracked
227
+ DESC
228
+
229
+ option ["--details"], :flag, "Show detailed information about each repository"
230
+ option ["--paths"], :flag, "Show local paths for each repository"
231
+ option ["--tracked"], :flag, "Show tracked repository URLs (not cloned)"
232
+ option ["--all"], :flag, "Show both cloned and tracked repositories"
233
+ option ["--format"], "FORMAT", "Output format (table, json, plain)", default: "table"
234
+
235
+ def execute
236
+ repositories = discover_repositories
237
+
238
+ if repositories.empty?
239
+ puts "No git repositories found."
240
+ puts "Use 'makit repository clone <url>' to clone repositories."
241
+ return
242
+ end
243
+
244
+ case format
245
+ when "json"
246
+ output_json(repositories)
247
+ when "plain"
248
+ output_plain(repositories)
249
+ else
250
+ output_table(repositories)
251
+ end
252
+ end
253
+
254
+ private
255
+
256
+ def discover_repositories
257
+ repositories = []
258
+
259
+ if tracked? && !all?
260
+ repositories = load_tracked_repositories
261
+ elsif all?
262
+ repositories = discover_cloned_repositories
263
+ tracked_repos = load_tracked_repositories
264
+ tracked_repos.each do |tracked_repo|
265
+ unless repositories.any? { |repo| repo[:url] == tracked_repo[:url] }
266
+ repositories << tracked_repo.merge(type: "tracked")
267
+ end
268
+ end
269
+ else
270
+ repositories = discover_cloned_repositories
271
+ end
272
+
273
+ repositories.sort_by { |repo| repo[:url] || repo[:name] }
274
+ end
275
+
276
+ def discover_cloned_repositories
277
+ repositories = []
278
+ clone_base_dir = Makit::Directories::CLONE
279
+ return repositories unless Dir.exist?(clone_base_dir)
280
+
281
+ Find.find(clone_base_dir) do |path|
282
+ next unless File.directory?(path) && File.basename(path) == ".git"
283
+
284
+ repo_dir = File.dirname(path)
285
+ repo_info = extract_repository_info(repo_dir)
286
+ if repo_info
287
+ repo_info[:type] = "cloned"
288
+ repositories << repo_info
289
+ end
290
+ Find.prune
291
+ end
292
+
293
+ repositories
294
+ end
295
+
296
+ def load_tracked_repositories
297
+ tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
298
+ return [] unless File.exist?(tracked_file)
299
+
300
+ begin
301
+ tracked_repos = JSON.parse(File.read(tracked_file))
302
+ tracked_repos.map do |repo|
303
+ {
304
+ name: extract_repo_name_from_url(repo["url"]),
305
+ url: repo["url"],
306
+ added_at: repo["added_at"],
307
+ status: repo["status"],
308
+ type: "tracked",
309
+ }
310
+ end
311
+ rescue JSON::ParserError => e
312
+ warn "Warning: Could not parse tracked repositories file: #{e.message}"
313
+ []
314
+ end
315
+ end
316
+
317
+ def extract_repo_name_from_url(url)
318
+ if url.include?("/")
319
+ url.split("/").last.sub(/\.git$/, "")
320
+ else
321
+ url
322
+ end
323
+ end
324
+
325
+ def extract_repository_info(repo_dir)
326
+ return nil unless Dir.exist?(repo_dir)
327
+
328
+ info = {
329
+ name: File.basename(repo_dir),
330
+ path: repo_dir,
331
+ relative_path: repo_dir.sub("#{Makit::Directories::CLONE}/", ""),
332
+ }
333
+
334
+ begin
335
+ Dir.chdir(repo_dir) do
336
+ remote_output = `git remote get-url origin 2>/dev/null`.strip
337
+ info[:url] = remote_output unless remote_output.empty?
338
+
339
+ if details?
340
+ info[:branch] = `git branch --show-current 2>/dev/null`.strip
341
+ info[:last_commit] = `git log -1 --format="%H %s" 2>/dev/null`.strip
342
+ info[:last_commit_date] = `git log -1 --format="%ci" 2>/dev/null`.strip
343
+ info[:status] = `git status --porcelain 2>/dev/null`.strip
344
+ info[:clean] = info[:status].empty?
345
+ end
346
+ end
347
+ rescue StandardError => e
348
+ info[:error] = "Cannot read git info: #{e.message}"
349
+ end
350
+
351
+ info
352
+ end
353
+
354
+ def output_table(repositories)
355
+ if details?
356
+ output_detailed_table(repositories)
357
+ else
358
+ output_simple_table(repositories)
359
+ end
360
+ end
361
+
362
+ def output_simple_table(repositories)
363
+ puts "Tracked Git Repositories (#{repositories.length}):"
364
+ puts "=" * 80
365
+
366
+ repositories.each_with_index do |repo, index|
367
+ type_indicator = repo[:type] == "tracked" ? "📋" : "📁"
368
+ puts "#{(index + 1).to_s.rjust(3)}. #{type_indicator} #{repo[:url] || repo[:name]}"
369
+ puts " Path: #{repo[:path] || "Not cloned"}" if paths?
370
+ puts " Type: #{repo[:type]&.capitalize || "Unknown"}" if all? || tracked?
371
+ puts
372
+ end
373
+ end
374
+
375
+ def output_detailed_table(repositories)
376
+ puts "Tracked Git Repositories (#{repositories.length}):"
377
+ puts "=" * 120
378
+
379
+ repositories.each_with_index do |repo, index|
380
+ type_indicator = repo[:type] == "tracked" ? "📋" : "📁"
381
+ puts "#{(index + 1).to_s.rjust(3)}. #{type_indicator} #{repo[:url] || repo[:name]}"
382
+ puts " Type: #{repo[:type]&.capitalize || "Unknown"}"
383
+ puts " Path: #{repo[:path] || "Not cloned"}" if repo[:type] != "tracked" || all?
384
+ puts " Branch: #{repo[:branch]}" if repo[:branch] && !repo[:branch].empty?
385
+ puts " Status: #{repo[:clean] ? "Clean" : "Modified"}" if repo[:type] == "cloned"
386
+
387
+ if repo[:last_commit] && !repo[:last_commit].empty?
388
+ commit_parts = repo[:last_commit].split(" ", 2)
389
+ commit_hash = commit_parts[0][0..7] if commit_parts[0]
390
+ commit_msg = commit_parts[1] if commit_parts[1]
391
+ puts " Commit: #{commit_hash} - #{commit_msg}"
392
+ end
393
+
394
+ puts " Date: #{repo[:last_commit_date]}" if repo[:last_commit_date] && !repo[:last_commit_date].empty?
395
+ puts " Added: #{repo[:added_at]}" if repo[:added_at]
396
+ puts " Error: #{repo[:error]}" if repo[:error]
397
+ puts
398
+ end
399
+ end
400
+
401
+ def output_json(repositories)
402
+ puts JSON.pretty_generate({
403
+ count: repositories.length,
404
+ repositories: repositories,
405
+ })
406
+ end
407
+
408
+ def output_plain(repositories)
409
+ repositories.each do |repo|
410
+ if paths?
411
+ puts "#{repo[:url] || repo[:name]} -> #{repo[:path]}"
412
+ else
413
+ puts repo[:url] || repo[:name]
414
+ end
415
+ end
416
+ end
417
+ end
418
+
419
+ # Remove tracked repositories by pattern
420
+ class RemoveRepositoryCommand < Clamp::Command
421
+ self.description = <<~DESC
422
+ Remove tracked repositories using pattern matching.
423
+
424
+ Examples:
425
+ makit repository remove github.com/user/
426
+ makit repository remove clamp.git
427
+ makit repository remove --missing-rakefile
428
+ DESC
429
+
430
+ parameter "PATTERN", "Pattern to match repository URLs", attribute_name: :pattern, required: false
431
+ option ["--missing-rakefile"], :flag, "Remove repositories without Rakefile"
432
+ option ["--dry-run"], :flag, "Show what would be removed without actually removing"
433
+ option ["--verbose"], :flag, "Show verbose output"
434
+
435
+ def execute
436
+ if missing_rakefile?
437
+ remove_missing_rakefile_repos
438
+ elsif pattern
439
+ remove_by_pattern(pattern)
440
+ else
441
+ puts "Error: Either provide a PATTERN or use --missing-rakefile"
442
+ exit 1
443
+ end
444
+ end
445
+
446
+ private
447
+
448
+ def remove_by_pattern(pattern)
449
+ tracked_repos = load_tracked_repositories
450
+ matching_repos = tracked_repos.select { |repo| repo["url"].include?(pattern) }
451
+
452
+ if matching_repos.empty?
453
+ puts "No repositories found matching pattern: #{pattern}"
454
+ return
455
+ end
456
+
457
+ puts "Found #{matching_repos.length} repositories matching pattern: #{pattern}"
458
+ matching_repos.each { |repo| puts " - #{repo["url"]}" }
459
+
460
+ return if dry_run?
461
+
462
+ remaining_repos = tracked_repos.reject { |repo| repo["url"].include?(pattern) }
463
+ save_tracked_repositories(remaining_repos)
464
+ puts "✅ Removed #{matching_repos.length} repositories"
465
+ end
466
+
467
+ def remove_missing_rakefile_repos
468
+ tracked_repos = load_tracked_repositories
469
+ repos_to_remove = []
470
+
471
+ tracked_repos.each do |repo|
472
+ clone_dir = Makit::Directories.get_clone_directory(repo["url"])
473
+ rakefile_path = File.join(clone_dir, "Rakefile")
474
+
475
+ if Dir.exist?(clone_dir) && !File.exist?(rakefile_path)
476
+ repos_to_remove << repo
477
+ puts " - #{repo["url"]} (no Rakefile)" if verbose?
478
+ end
479
+ end
480
+
481
+ if repos_to_remove.empty?
482
+ puts "No repositories found without Rakefile"
483
+ return
484
+ end
485
+
486
+ puts "Found #{repos_to_remove.length} repositories without Rakefile"
487
+ return if dry_run?
488
+
489
+ remaining_repos = tracked_repos - repos_to_remove
490
+ save_tracked_repositories(remaining_repos)
491
+ puts "✅ Removed #{repos_to_remove.length} repositories without Rakefile"
492
+ end
493
+
494
+ def load_tracked_repositories
495
+ tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
496
+ return [] unless File.exist?(tracked_file)
497
+
498
+ begin
499
+ JSON.parse(File.read(tracked_file))
500
+ rescue JSON::ParserError => e
501
+ warn "Warning: Could not parse tracked repositories file: #{e.message}"
502
+ []
503
+ end
504
+ end
505
+
506
+ def save_tracked_repositories(repositories)
507
+ tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
508
+ FileUtils.mkdir_p(File.dirname(tracked_file))
509
+ sorted_repos = repositories.sort_by { |repo| repo["url"] }
510
+ File.write(tracked_file, JSON.pretty_generate(sorted_repos))
511
+ end
512
+ end
513
+
514
+ # Import repositories from filesystem
515
+ class ImportRepositoryCommand < Clamp::Command
516
+ self.description = <<~DESC
517
+ Import git repositories from filesystem by discovery.
518
+
519
+ Examples:
520
+ makit repository import
521
+ makit repository import --directory ~/code
522
+ makit repository import --skip-rakefile-check
523
+ DESC
524
+
525
+ option ["--directory"], "DIR", "Directory to search for repositories", default: File.join(Dir.home, "code")
526
+ option ["--max-depth"], "DEPTH", "Maximum search depth", default: 3, &method(:Integer)
527
+ option ["--skip-rakefile-check"], :flag, "Import all repositories, not just those with Rakefile"
528
+ option ["--dry-run"], :flag, "Show what would be imported without actually importing"
529
+ option ["--verbose"], :flag, "Show verbose output"
530
+
531
+ def execute
532
+ search_dir = directory
533
+ unless Dir.exist?(search_dir)
534
+ puts "Directory does not exist: #{search_dir}"
535
+ exit 1
536
+ end
537
+
538
+ puts "Searching for git repositories in: #{search_dir}"
539
+ puts "Max depth: #{max_depth}"
540
+ puts "Rakefile required: #{!skip_rakefile_check?}"
541
+
542
+ repositories = discover_repositories(search_dir)
543
+
544
+ if repositories.empty?
545
+ puts "No repositories found"
546
+ return
547
+ end
548
+
549
+ puts "\nFound #{repositories.length} repositories:"
550
+ repositories.each { |repo| puts " - #{repo[:url] || repo[:path]}" }
551
+
552
+ return if dry_run?
553
+
554
+ import_repositories(repositories)
555
+ end
556
+
557
+ private
558
+
559
+ def discover_repositories(base_dir)
560
+ repositories = []
561
+
562
+ Find.find(base_dir) do |path|
563
+ next unless File.directory?(path) && File.basename(path) == ".git"
564
+
565
+ repo_dir = File.dirname(path)
566
+
567
+ # Check depth
568
+ relative_path = repo_dir.sub("#{base_dir}/", "")
569
+ depth = relative_path.split("/").length
570
+
571
+ if depth <= max_depth
572
+ repo_info = extract_repository_info(repo_dir)
573
+ repositories << repo_info if repo_info && should_import?(repo_dir)
574
+ end
575
+
576
+ Find.prune
577
+ end
578
+
579
+ repositories
580
+ end
581
+
582
+ def should_import?(repo_dir)
583
+ return true if skip_rakefile_check?
584
+
585
+ File.exist?(File.join(repo_dir, "Rakefile"))
586
+ end
587
+
588
+ def extract_repository_info(repo_dir)
589
+ return nil unless Dir.exist?(repo_dir)
590
+
591
+ info = { path: repo_dir }
592
+
593
+ begin
594
+ Dir.chdir(repo_dir) do
595
+ remote_output = `git remote get-url origin 2>/dev/null`.strip
596
+ info[:url] = remote_output unless remote_output.empty?
597
+ end
598
+ rescue StandardError
599
+ # If we can't get git info, skip this repo
600
+ return nil
601
+ end
602
+
603
+ info
604
+ end
605
+
606
+ def import_repositories(repositories)
607
+ tracked_repos = load_tracked_repositories
608
+ imported_count = 0
609
+
610
+ repositories.each do |repo|
611
+ next unless repo[:url]
612
+ next if tracked_repos.any? { |tracked| tracked["url"] == repo[:url] }
613
+
614
+ new_repo = {
615
+ "url" => repo[:url],
616
+ "added_at" => Time.now.iso8601,
617
+ "status" => "imported",
618
+ }
619
+
620
+ tracked_repos << new_repo
621
+ imported_count += 1
622
+ puts " ✅ Imported: #{repo[:url]}" if verbose?
623
+ end
624
+
625
+ if imported_count.positive?
626
+ save_tracked_repositories(tracked_repos)
627
+ puts "✅ Successfully imported #{imported_count} repositories"
628
+ else
629
+ puts "No new repositories to import"
630
+ end
631
+ end
632
+
633
+ def load_tracked_repositories
634
+ tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
635
+ return [] unless File.exist?(tracked_file)
636
+
637
+ begin
638
+ JSON.parse(File.read(tracked_file))
639
+ rescue JSON::ParserError => e
640
+ warn "Warning: Could not parse tracked repositories file: #{e.message}"
641
+ []
642
+ end
643
+ end
644
+
645
+ def save_tracked_repositories(repositories)
646
+ tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
647
+ FileUtils.mkdir_p(File.dirname(tracked_file))
648
+ sorted_repos = repositories.sort_by { |repo| repo["url"] }
649
+ File.write(tracked_file, JSON.pretty_generate(sorted_repos))
650
+ end
651
+ end
652
+
653
+ # Add subcommand declarations after all classes are defined
654
+ RepositoryCommand.subcommand "add", "Add repository to tracking list", AddRepositoryCommand
655
+ RepositoryCommand.subcommand "clone", "Clone repository to local directory", CloneRepositoryCommand
656
+ RepositoryCommand.subcommand "pull", "Pull latest changes", PullRepositoryCommand
657
+ RepositoryCommand.subcommand "list", "List tracked repositories", ListRepositoryCommand
658
+ RepositoryCommand.subcommand "remove", "Remove tracked repositories", RemoveRepositoryCommand
659
+ RepositoryCommand.subcommand "import", "Import repositories from filesystem", ImportRepositoryCommand
660
+ end
661
+ end