ukiryu 0.1.1 → 0.1.3

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +58 -14
  3. data/.gitignore +3 -0
  4. data/.rubocop_todo.yml +170 -79
  5. data/Gemfile +1 -1
  6. data/README.adoc +1603 -576
  7. data/docs/.gitignore +1 -0
  8. data/docs/Gemfile +10 -0
  9. data/docs/INDEX.adoc +261 -0
  10. data/docs/_config.yml +180 -0
  11. data/docs/advanced/custom-tool-classes.adoc +581 -0
  12. data/docs/advanced/index.adoc +20 -0
  13. data/docs/features/configuration.adoc +657 -0
  14. data/docs/features/index.adoc +31 -0
  15. data/docs/features/platform-support.adoc +488 -0
  16. data/docs/getting-started/core-concepts.adoc +666 -0
  17. data/docs/getting-started/index.adoc +36 -0
  18. data/docs/getting-started/installation.adoc +216 -0
  19. data/docs/getting-started/quick-start.adoc +258 -0
  20. data/docs/guides/env-var-sets.adoc +388 -0
  21. data/docs/guides/index.adoc +20 -0
  22. data/docs/interfaces/cli.adoc +609 -0
  23. data/docs/interfaces/index.adoc +153 -0
  24. data/docs/interfaces/ruby-api.adoc +538 -0
  25. data/docs/lychee.toml +49 -0
  26. data/docs/reference/configuration-options.adoc +720 -0
  27. data/docs/reference/error-codes.adoc +634 -0
  28. data/docs/reference/index.adoc +20 -0
  29. data/docs/reference/ruby-api.adoc +1217 -0
  30. data/docs/understanding/index.adoc +20 -0
  31. data/lib/ukiryu/cli.rb +43 -58
  32. data/lib/ukiryu/cli_commands/base_command.rb +16 -27
  33. data/lib/ukiryu/cli_commands/cache_command.rb +100 -0
  34. data/lib/ukiryu/cli_commands/commands_command.rb +8 -8
  35. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +1 -1
  36. data/lib/ukiryu/cli_commands/config_command.rb +49 -7
  37. data/lib/ukiryu/cli_commands/definitions_command.rb +254 -0
  38. data/lib/ukiryu/cli_commands/describe_command.rb +13 -7
  39. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +1 -1
  40. data/lib/ukiryu/cli_commands/docs_command.rb +148 -0
  41. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +1 -1
  42. data/lib/ukiryu/cli_commands/extract_command.rb +2 -2
  43. data/lib/ukiryu/cli_commands/info_command.rb +7 -7
  44. data/lib/ukiryu/cli_commands/lint_command.rb +167 -0
  45. data/lib/ukiryu/cli_commands/list_command.rb +6 -6
  46. data/lib/ukiryu/cli_commands/opts_command.rb +2 -2
  47. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +1 -1
  48. data/lib/ukiryu/cli_commands/register_command.rb +144 -0
  49. data/lib/ukiryu/cli_commands/resolve_command.rb +124 -0
  50. data/lib/ukiryu/cli_commands/run_command.rb +38 -14
  51. data/lib/ukiryu/cli_commands/run_file_command.rb +2 -2
  52. data/lib/ukiryu/cli_commands/system_command.rb +50 -32
  53. data/lib/ukiryu/cli_commands/validate_command.rb +452 -51
  54. data/lib/ukiryu/cli_commands/which_command.rb +5 -5
  55. data/lib/ukiryu/command_builder.rb +81 -23
  56. data/lib/ukiryu/config/env_provider.rb +3 -3
  57. data/lib/ukiryu/config/env_schema.rb +6 -6
  58. data/lib/ukiryu/config.rb +11 -11
  59. data/lib/ukiryu/definition/definition_cache.rb +238 -0
  60. data/lib/ukiryu/definition/definition_composer.rb +257 -0
  61. data/lib/ukiryu/definition/definition_linter.rb +460 -0
  62. data/lib/ukiryu/definition/definition_validator.rb +320 -0
  63. data/lib/ukiryu/definition/discovery.rb +239 -0
  64. data/lib/ukiryu/definition/documentation_generator.rb +429 -0
  65. data/lib/ukiryu/definition/lint_issue.rb +168 -0
  66. data/lib/ukiryu/definition/loader.rb +139 -0
  67. data/lib/ukiryu/definition/metadata.rb +159 -0
  68. data/lib/ukiryu/definition/source.rb +87 -0
  69. data/lib/ukiryu/definition/sources/file.rb +138 -0
  70. data/lib/ukiryu/definition/sources/string.rb +88 -0
  71. data/lib/ukiryu/definition/validation_result.rb +158 -0
  72. data/lib/ukiryu/definition/version_resolver.rb +194 -0
  73. data/lib/ukiryu/definition.rb +40 -0
  74. data/lib/ukiryu/errors.rb +6 -0
  75. data/lib/ukiryu/execution_context.rb +11 -11
  76. data/lib/ukiryu/executor.rb +6 -0
  77. data/lib/ukiryu/extractors/extractor.rb +6 -5
  78. data/lib/ukiryu/extractors/help_parser.rb +13 -19
  79. data/lib/ukiryu/logger.rb +3 -1
  80. data/lib/ukiryu/models/command_definition.rb +3 -3
  81. data/lib/ukiryu/models/command_info.rb +1 -1
  82. data/lib/ukiryu/models/components.rb +1 -3
  83. data/lib/ukiryu/models/env_var_definition.rb +11 -3
  84. data/lib/ukiryu/models/flag_definition.rb +15 -0
  85. data/lib/ukiryu/models/option_definition.rb +7 -7
  86. data/lib/ukiryu/models/platform_profile.rb +6 -3
  87. data/lib/ukiryu/models/routing.rb +1 -1
  88. data/lib/ukiryu/models/tool_definition.rb +2 -4
  89. data/lib/ukiryu/models/tool_metadata.rb +6 -6
  90. data/lib/ukiryu/models/validation_result.rb +1 -1
  91. data/lib/ukiryu/models/version_compatibility.rb +6 -3
  92. data/lib/ukiryu/models/version_detection.rb +10 -1
  93. data/lib/ukiryu/{registry.rb → register.rb} +54 -38
  94. data/lib/ukiryu/register_auto_manager.rb +268 -0
  95. data/lib/ukiryu/schema_validator.rb +31 -10
  96. data/lib/ukiryu/shell/base.rb +18 -0
  97. data/lib/ukiryu/shell/bash.rb +19 -1
  98. data/lib/ukiryu/shell/cmd.rb +11 -1
  99. data/lib/ukiryu/shell/powershell.rb +11 -1
  100. data/lib/ukiryu/shell.rb +1 -1
  101. data/lib/ukiryu/tool.rb +107 -95
  102. data/lib/ukiryu/tool_index.rb +22 -22
  103. data/lib/ukiryu/tools/base.rb +12 -25
  104. data/lib/ukiryu/tools/generator.rb +7 -7
  105. data/lib/ukiryu/tools.rb +3 -3
  106. data/lib/ukiryu/type.rb +20 -5
  107. data/lib/ukiryu/version.rb +1 -1
  108. data/lib/ukiryu/version_detector.rb +21 -2
  109. data/lib/ukiryu.rb +6 -3
  110. data/ukiryu-proposal.md +41 -41
  111. data/ukiryu.gemspec +1 -0
  112. metadata +64 -8
  113. data/.gitmodules +0 -3
@@ -0,0 +1,460 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lint_issue'
4
+
5
+ module Ukiryu
6
+ module Definition
7
+ # Lint tool definitions for best practices
8
+ #
9
+ # This class checks tool definitions for best practices,
10
+ # deprecated patterns, naming conventions, and security issues.
11
+ class DefinitionLinter
12
+ # Linting result
13
+ class LintResult
14
+ attr_reader :issues
15
+
16
+ def initialize(issues = [])
17
+ @issues = issues
18
+ end
19
+
20
+ # Get issues by severity
21
+ #
22
+ # @param severity [Symbol] severity level
23
+ # @return [Array<LintIssue>] issues with the specified severity
24
+ def by_severity(severity)
25
+ @issues.select { |i| i.severity == severity }
26
+ end
27
+
28
+ # Get errors
29
+ #
30
+ # @return [Array<LintIssue>] error issues
31
+ def errors
32
+ by_severity(LintIssue::SEVERITY_ERROR)
33
+ end
34
+
35
+ # Get warnings
36
+ #
37
+ # @return [Array<LintIssue>] warning issues
38
+ def warnings
39
+ by_severity(LintIssue::SEVERITY_WARNING)
40
+ end
41
+
42
+ # Get info issues
43
+ #
44
+ # @return [Array<LintIssue>] info issues
45
+ def infos
46
+ by_severity(LintIssue::SEVERITY_INFO)
47
+ end
48
+
49
+ # Get style issues
50
+ #
51
+ # @return [Array<LintIssue>] style issues
52
+ def styles
53
+ by_severity(LintIssue::SEVERITY_STYLE)
54
+ end
55
+
56
+ # Check if there are any issues
57
+ #
58
+ # @return [Boolean] true if there are issues
59
+ def has_issues?
60
+ !@issues.empty?
61
+ end
62
+
63
+ # Check if there are any errors
64
+ #
65
+ # @return [Boolean] true if there are errors
66
+ def has_errors?
67
+ !errors.empty?
68
+ end
69
+
70
+ # Get total issue count
71
+ #
72
+ # @return [Integer] total number of issues
73
+ def count
74
+ @issues.length
75
+ end
76
+
77
+ # Get count by severity
78
+ #
79
+ # @param severity [Symbol] severity level
80
+ # @return [Integer] count of issues with specified severity
81
+ def count_by_severity(severity)
82
+ by_severity(severity).length
83
+ end
84
+
85
+ # Convert to hash
86
+ #
87
+ # @return [Hash] hash representation
88
+ def to_h
89
+ {
90
+ issues: @issues.map(&:to_h),
91
+ total_count: count,
92
+ error_count: errors.length,
93
+ warning_count: warnings.length,
94
+ info_count: infos.length,
95
+ style_count: styles.length
96
+ }
97
+ end
98
+
99
+ # Format as string
100
+ #
101
+ # @return [String] formatted result
102
+ def to_s
103
+ return 'No issues found' unless has_issues?
104
+
105
+ output = []
106
+ output << "Found #{count} issue(s):"
107
+
108
+ {
109
+ LintIssue::SEVERITY_ERROR => errors,
110
+ LintIssue::SEVERITY_WARNING => warnings,
111
+ LintIssue::SEVERITY_INFO => infos,
112
+ LintIssue::SEVERITY_STYLE => styles
113
+ }.each do |severity, issues|
114
+ next if issues.empty?
115
+
116
+ output << ''
117
+ output << "#{severity.to_s.upcase}:"
118
+ issues.each { |issue| output << " #{issue}" }
119
+ end
120
+
121
+ output.join("\n")
122
+ end
123
+ end
124
+
125
+ # Linting rules configuration
126
+ class Rules
127
+ # Naming convention rules
128
+ NAMING_RULES = {
129
+ tool_name_format: {
130
+ rule_id: 'naming_tool_name_format',
131
+ pattern: /^[a-z][a-z0-9_-]*$/,
132
+ message: 'Tool name should start with a lowercase letter and contain only lowercase letters, numbers, hyphens, and underscores',
133
+ suggestion: 'Use lowercase with hyphens for multi-word names (e.g., "my-tool")'
134
+ },
135
+ command_name_format: {
136
+ rule_id: 'naming_command_name_format',
137
+ pattern: /^[a-z][a-z0-9_]*$/,
138
+ message: 'Command names should be lowercase with underscores',
139
+ suggestion: 'Use snake_case for command names (e.g., "build_command")'
140
+ }
141
+ }.freeze
142
+
143
+ # Completeness rules
144
+ COMPLETENESS_RULES = {
145
+ missing_description: {
146
+ rule_id: 'complete_missing_description',
147
+ message: 'Tool is missing a description',
148
+ suggestion: 'Add a "description" field to help users understand what this tool does'
149
+ },
150
+ missing_homepage: {
151
+ rule_id: 'complete_missing_homepage',
152
+ message: 'Tool is missing a homepage URL',
153
+ suggestion: 'Add a "homepage" field linking to the tool\'s website'
154
+ },
155
+ missing_version_detection: {
156
+ rule_id: 'complete_missing_version_detection',
157
+ message: 'Tool is missing version detection',
158
+ suggestion: 'Add "version_detection" to auto-detect installed versions'
159
+ }
160
+ }.freeze
161
+
162
+ # Security rules
163
+ SECURITY_RULES = {
164
+ suspicious_subcommand: {
165
+ rule_id: 'security_suspicious_subcommand',
166
+ pattern: /(^|\s|;)\s*(rm\s+-rf|del|format|mkfs)/,
167
+ message: 'Subcommand contains potentially dangerous shell commands',
168
+ suggestion: 'Avoid using destructive commands in subcommands'
169
+ },
170
+ unvalidated_user_input: {
171
+ rule_id: 'security_unvalidated_input',
172
+ message: 'Arguments should specify type validation',
173
+ suggestion: 'Add "type" field to all arguments for validation'
174
+ }
175
+ }.freeze
176
+
177
+ # Deprecated patterns
178
+ DEPRECATED_RULES = {
179
+ old_schema_version: {
180
+ rule_id: 'deprecated_old_schema',
181
+ threshold: '1.0',
182
+ message: 'Using old schema version',
183
+ suggestion: 'Update to the latest schema version (1.2)'
184
+ }
185
+ }.freeze
186
+ end
187
+
188
+ class << self
189
+ # Lint a definition
190
+ #
191
+ # @param definition [Hash] the definition to lint
192
+ # @param rules [Hash, nil] optional rule overrides
193
+ # @return [LintResult] linting result
194
+ def lint(definition, rules: nil)
195
+ issues = []
196
+
197
+ # Check if definition is a hash
198
+ return LintResult.new([LintIssue.error('Definition must be a hash/object')]) unless definition.is_a?(Hash)
199
+
200
+ # Run all lint checks
201
+ issues.concat(check_naming_conventions(definition))
202
+ issues.concat(check_completeness(definition))
203
+ issues.concat(check_security(definition))
204
+ issues.concat(check_deprecated_patterns(definition))
205
+ issues.concat(check_best_practices(definition))
206
+
207
+ # Filter by rules if provided
208
+ if rules
209
+ enabled_rules = rules[:enabled] || []
210
+ disabled_rules = rules[:disabled] || []
211
+
212
+ issues = issues.select do |issue|
213
+ if disabled_rules.any?
214
+ !disabled_rules.include?(issue.rule_id)
215
+ elsif enabled_rules.any?
216
+ enabled_rules.include?(issue.rule_id)
217
+ else
218
+ true
219
+ end
220
+ end
221
+ end
222
+
223
+ LintResult.new(issues)
224
+ end
225
+
226
+ # Lint a definition file
227
+ #
228
+ # @param file_path [String] path to definition file
229
+ # @param rules [Hash, nil] optional rule overrides
230
+ # @return [LintResult] linting result
231
+ def lint_file(file_path, rules: nil)
232
+ # Load raw YAML hash for linting
233
+ require 'yaml'
234
+ definition = YAML.safe_load(File.read(file_path), permitted_classes: [Symbol, Date, Time],
235
+ symbolize_names: true)
236
+ lint(definition, rules: rules)
237
+ rescue Ukiryu::DefinitionNotFoundError
238
+ LintResult.new([LintIssue.error("File not found: #{file_path}")])
239
+ rescue Ukiryu::DefinitionLoadError, Ukiryu::DefinitionValidationError => e
240
+ LintResult.new([LintIssue.error(e.message)])
241
+ rescue Errno::ENOENT
242
+ LintResult.new([LintIssue.error("File not found: #{file_path}")])
243
+ rescue Psych::SyntaxError => e
244
+ LintResult.new([LintIssue.error("Invalid YAML: #{e.message}")])
245
+ end
246
+
247
+ # Lint a YAML string
248
+ #
249
+ # @param yaml_string [String] YAML content
250
+ # @param rules [Hash, nil] optional rule overrides
251
+ # @return [LintResult] linting result
252
+ def lint_string(yaml_string, rules: nil)
253
+ require 'yaml'
254
+ definition = YAML.safe_load(yaml_string, permitted_classes: [Symbol, Date, Time])
255
+ lint(definition, rules: rules)
256
+ rescue Psych::SyntaxError => e
257
+ LintResult.new([LintIssue.error("Invalid YAML: #{e.message}")])
258
+ end
259
+
260
+ private
261
+
262
+ # Check naming conventions
263
+ #
264
+ # @param definition [Hash] the definition
265
+ # @return [Array<LintIssue>] naming issues
266
+ def check_naming_conventions(definition)
267
+ issues = []
268
+
269
+ # Check tool name format
270
+ if definition[:name]
271
+ rule = Rules::NAMING_RULES[:tool_name_format]
272
+ unless definition[:name].match?(rule[:pattern])
273
+ issues << LintIssue.warning(
274
+ rule[:message],
275
+ location: 'name',
276
+ suggestion: rule[:suggestion],
277
+ rule_id: rule[:rule_id]
278
+ )
279
+ end
280
+ end
281
+
282
+ # Check command names
283
+ definition[:profiles]&.each_with_index do |profile, p_idx|
284
+ next unless profile[:commands]
285
+
286
+ profile[:commands].each_key do |cmd_name|
287
+ rule = Rules::NAMING_RULES[:command_name_format]
288
+ next if cmd_name.to_s.match?(rule[:pattern])
289
+
290
+ issues << LintIssue.warning(
291
+ rule[:message],
292
+ location: "profiles[#{p_idx}].commands.#{cmd_name}",
293
+ suggestion: rule[:suggestion],
294
+ rule_id: rule[:rule_id]
295
+ )
296
+ end
297
+ end
298
+
299
+ issues
300
+ end
301
+
302
+ # Check completeness
303
+ #
304
+ # @param definition [Hash] the definition
305
+ # @return [Array<LintIssue>] completeness issues
306
+ def check_completeness(definition)
307
+ issues = []
308
+
309
+ # Check for description
310
+ unless definition[:description]
311
+ rule = Rules::COMPLETENESS_RULES[:missing_description]
312
+ issues << LintIssue.info(
313
+ rule[:message],
314
+ location: 'definition',
315
+ suggestion: rule[:suggestion],
316
+ rule_id: rule[:rule_id]
317
+ )
318
+ end
319
+
320
+ # Check for homepage
321
+ unless definition[:homepage]
322
+ rule = Rules::COMPLETENESS_RULES[:missing_homepage]
323
+ issues << LintIssue.info(
324
+ rule[:message],
325
+ location: 'definition',
326
+ suggestion: rule[:suggestion],
327
+ rule_id: rule[:rule_id]
328
+ )
329
+ end
330
+
331
+ # Check for version detection
332
+ unless definition[:version_detection]
333
+ rule = Rules::COMPLETENESS_RULES[:missing_version_detection]
334
+ issues << LintIssue.warning(
335
+ rule[:message],
336
+ location: 'definition',
337
+ suggestion: rule[:suggestion],
338
+ rule_id: rule[:rule_id]
339
+ )
340
+ end
341
+
342
+ issues
343
+ end
344
+
345
+ # Check security issues
346
+ #
347
+ # @param definition [Hash] the definition
348
+ # @return [Array<LintIssue>] security issues
349
+ def check_security(definition)
350
+ issues = []
351
+
352
+ # Check for suspicious subcommands
353
+ definition[:profiles]&.each_with_index do |profile, p_idx|
354
+ next unless profile[:commands]
355
+
356
+ profile[:commands].each do |cmd_name, cmd_def|
357
+ next unless cmd_def[:subcommand]
358
+
359
+ subcommand = cmd_def[:subcommand].to_s
360
+ rule = Rules::SECURITY_RULES[:suspicious_subcommand]
361
+ next unless subcommand.match?(rule[:pattern])
362
+
363
+ issues << LintIssue.error(
364
+ rule[:message],
365
+ location: "profiles[#{p_idx}].commands.#{cmd_name}.subcommand",
366
+ suggestion: rule[:suggestion],
367
+ rule_id: rule[:rule_id]
368
+ )
369
+ end
370
+
371
+ # Check for unvalidated arguments
372
+ next unless profile[:commands]
373
+
374
+ profile[:commands].each do |cmd_name, cmd_def|
375
+ next unless cmd_def[:arguments]
376
+
377
+ cmd_def[:arguments].each_with_index do |arg, a_idx|
378
+ next if arg[:type]
379
+
380
+ rule = Rules::SECURITY_RULES[:unvalidated_user_input]
381
+ issues << LintIssue.warning(
382
+ rule[:message],
383
+ location: "profiles[#{p_idx}].commands.#{cmd_name}.arguments[#{a_idx}]",
384
+ suggestion: rule[:suggestion],
385
+ rule_id: rule[:rule_id]
386
+ )
387
+ end
388
+ end
389
+ end
390
+
391
+ issues
392
+ end
393
+
394
+ # Check for deprecated patterns
395
+ #
396
+ # @param definition [Hash] the definition
397
+ # @return [Array<LintIssue>] deprecation issues
398
+ def check_deprecated_patterns(definition)
399
+ issues = []
400
+
401
+ # Check schema version
402
+ if definition[:schema] || definition['$schema']
403
+ schema = definition[:schema] || definition['$schema']
404
+ version = schema.to_s.split('/').last.gsub('v', '')
405
+
406
+ require 'ukiryu/definition/version_resolver'
407
+ rule = Rules::DEPRECATED_RULES[:old_schema_version]
408
+
409
+ begin
410
+ if VersionResolver.compare_versions(version, rule[:threshold]).negative?
411
+ issues << LintIssue.warning(
412
+ rule[:message],
413
+ location: 'schema',
414
+ suggestion: rule[:suggestion],
415
+ rule_id: rule[:rule_id]
416
+ )
417
+ end
418
+ rescue StandardError
419
+ # Skip if version comparison fails
420
+ end
421
+ end
422
+
423
+ issues
424
+ end
425
+
426
+ # Check best practices
427
+ #
428
+ # @param definition [Hash] the definition
429
+ # @return [Array<LintIssue>] best practice issues
430
+ def check_best_practices(definition)
431
+ issues = []
432
+
433
+ # Check for redundant default profile name
434
+ definition[:profiles]&.each_with_index do |profile, p_idx|
435
+ next unless profile[:name] == 'default' && definition[:profiles].length == 1
436
+
437
+ issues << LintIssue.style(
438
+ 'Single profile named "default" is redundant',
439
+ location: "profiles[#{p_idx}]",
440
+ suggestion: 'For single-profile definitions, omit the profiles array and define commands directly',
441
+ rule_id: 'style_redundant_default_profile'
442
+ )
443
+
444
+ # Check for missing platforms
445
+ next if profile[:platforms]
446
+
447
+ issues << LintIssue.warning(
448
+ 'Profile missing platforms specification',
449
+ location: "profiles[#{p_idx}]",
450
+ suggestion: 'Specify supported platforms (macos, linux, windows)',
451
+ rule_id: 'complete_missing_platforms'
452
+ )
453
+ end
454
+
455
+ issues
456
+ end
457
+ end
458
+ end
459
+ end
460
+ end