docopslab-dev 0.1.0

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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.adoc +904 -0
  4. data/assets/config-packs/actionlint/base.yml +13 -0
  5. data/assets/config-packs/actionlint/project.yml +13 -0
  6. data/assets/config-packs/htmlproofer/base.yml +27 -0
  7. data/assets/config-packs/htmlproofer/project.yml +25 -0
  8. data/assets/config-packs/rubocop/base.yml +130 -0
  9. data/assets/config-packs/rubocop/project.yml +8 -0
  10. data/assets/config-packs/shellcheck/base.shellcheckrc +14 -0
  11. data/assets/config-packs/subtxt/ai-asciidoc-antipatterns.sub.txt +11 -0
  12. data/assets/config-packs/vale/asciidoc/ExplicitSectionIDs.yml +8 -0
  13. data/assets/config-packs/vale/asciidoc/ExtraLineBeforeLevel1.yml +7 -0
  14. data/assets/config-packs/vale/asciidoc/OneSentencePerLine.yml +8 -0
  15. data/assets/config-packs/vale/asciidoc/PreferSourceBlocks.yml +8 -0
  16. data/assets/config-packs/vale/asciidoc/ProperAdmonitions.yml +8 -0
  17. data/assets/config-packs/vale/asciidoc/ProperDLs.yml +7 -0
  18. data/assets/config-packs/vale/asciidoc/UncleanListStart.yml +8 -0
  19. data/assets/config-packs/vale/authoring/ButParagraph.yml +8 -0
  20. data/assets/config-packs/vale/authoring/ExNotEg.yml +8 -0
  21. data/assets/config-packs/vale/authoring/LiteralTerms.yml +20 -0
  22. data/assets/config-packs/vale/authoring/Spelling.yml +679 -0
  23. data/assets/config-packs/vale/base.ini +38 -0
  24. data/assets/config-packs/vale/config/scripts/ExplicitSectionIDs.tengo +56 -0
  25. data/assets/config-packs/vale/config/scripts/ExtraLineBeforeLevel1.tengo +121 -0
  26. data/assets/config-packs/vale/config/scripts/OneSentencePerLine.tengo +53 -0
  27. data/assets/config-packs/vale/project.ini +5 -0
  28. data/assets/hooks/pre-commit +63 -0
  29. data/assets/hooks/pre-push +72 -0
  30. data/assets/scripts/adoc_section_ids.rb +50 -0
  31. data/assets/scripts/build-common.sh +193 -0
  32. data/assets/scripts/build-docker.sh +64 -0
  33. data/assets/scripts/build.sh +56 -0
  34. data/assets/scripts/parse_jekyll_asciidoc_logs.rb +467 -0
  35. data/assets/templates/Gemfile +7 -0
  36. data/assets/templates/Rakefile +3 -0
  37. data/assets/templates/gitignore +69 -0
  38. data/assets/templates/jekyll-asciidoc-fix.prompt.yml +17 -0
  39. data/assets/templates/spellcheck.prompt.yml +16 -0
  40. data/docopslab-dev.gemspec +56 -0
  41. data/docs/agent/AGENTS.md +229 -0
  42. data/docs/agent/index.md +80 -0
  43. data/docs/agent/missions/conduct-release.md +224 -0
  44. data/docs/agent/missions/setup-new-project.md +250 -0
  45. data/docs/agent/roles/devops-release-engineer.md +152 -0
  46. data/docs/agent/roles/docops-engineer.md +193 -0
  47. data/docs/agent/roles/planner-architect.md +74 -0
  48. data/docs/agent/roles/product-engineer.md +153 -0
  49. data/docs/agent/roles/product-manager.md +130 -0
  50. data/docs/agent/roles/project-manager.md +139 -0
  51. data/docs/agent/roles/qa-testing-engineer.md +115 -0
  52. data/docs/agent/roles/tech-docs-manager.md +143 -0
  53. data/docs/agent/roles/tech-writer.md +163 -0
  54. data/docs/agent/skills/asciidoc.md +609 -0
  55. data/docs/agent/skills/code-commenting.md +347 -0
  56. data/docs/agent/skills/fix-broken-links.md +309 -0
  57. data/docs/agent/skills/fix-jekyll-asciidoc-build-errors.md +23 -0
  58. data/docs/agent/skills/fix-spelling-issues.md +13 -0
  59. data/docs/agent/skills/git.md +170 -0
  60. data/docs/agent/skills/github-issues.md +135 -0
  61. data/docs/agent/skills/product-release-rollback-and-patching.md +71 -0
  62. data/docs/agent/skills/rake-cli-dev.md +57 -0
  63. data/docs/agent/skills/readme-driven-dev.md +13 -0
  64. data/docs/agent/skills/release-history.md +29 -0
  65. data/docs/agent/skills/ruby.md +192 -0
  66. data/docs/agent/skills/schemagraphy-sgyml.md +18 -0
  67. data/docs/agent/skills/tests-running.md +25 -0
  68. data/docs/agent/skills/tests-writing.md +45 -0
  69. data/docs/agent/skills/write-the-docs.md +54 -0
  70. data/docs/agent/topics/common-project-paths.md +117 -0
  71. data/docs/agent/topics/dev-tooling-usage.md +202 -0
  72. data/docs/agent/topics/devops-ci-cd.md +55 -0
  73. data/docs/agent/topics/product-docs-deployment.md +25 -0
  74. data/lib/docopslab/dev/auto_fix_asciidoc.rb +46 -0
  75. data/lib/docopslab/dev/checkers.rb +108 -0
  76. data/lib/docopslab/dev/config_manager.rb +241 -0
  77. data/lib/docopslab/dev/file_utils.rb +140 -0
  78. data/lib/docopslab/dev/git_hooks.rb +140 -0
  79. data/lib/docopslab/dev/help.rb +121 -0
  80. data/lib/docopslab/dev/initializer.rb +95 -0
  81. data/lib/docopslab/dev/linters.rb +451 -0
  82. data/lib/docopslab/dev/log_parser.rb +31 -0
  83. data/lib/docopslab/dev/paths.rb +46 -0
  84. data/lib/docopslab/dev/script_manager.rb +136 -0
  85. data/lib/docopslab/dev/spell_check.rb +194 -0
  86. data/lib/docopslab/dev/sync_ops.rb +468 -0
  87. data/lib/docopslab/dev/tasks.rb +440 -0
  88. data/lib/docopslab/dev/tool_execution.rb +68 -0
  89. data/lib/docopslab/dev/version.rb +8 -0
  90. data/lib/docopslab/dev.rb +392 -0
  91. data/specs/data/default-manifest.yml +64 -0
  92. data/specs/data/manifest-schema.yaml +63 -0
  93. data/specs/data/tasks-def.yml +321 -0
  94. data/specs/data/tools.yml +60 -0
  95. metadata +362 -0
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocOpsLab
4
+ module Dev
5
+ module ConfigManager
6
+ class << self
7
+ def generate_vale_config _context, style_override: nil
8
+ base_config = File.join(CONFIG_VENDOR_DIR, 'vale.ini')
9
+ project_config = '.config/vale.local.ini'
10
+ generated_config = CONFIG_PATHS[:vale]
11
+
12
+ return false unless File.exist?(base_config)
13
+
14
+ merged_content = if File.exist?(project_config)
15
+ merge_ini_configs(base_config, project_config)
16
+ else
17
+ File.read(base_config)
18
+ end
19
+
20
+ # Apply runtime style override if specified
21
+ merged_content = apply_vale_style_override(merged_content, style_override) if style_override
22
+
23
+ # Write generated config
24
+ if !File.exist?(generated_config) || File.read(generated_config) != merged_content
25
+ File.write(generated_config, merged_content)
26
+ override_msg = style_override ? " (#{style_override} styles)" : ''
27
+ puts " 📝 Generated #{generated_config} from base#{if File.exist?(project_config)
28
+ ' + local'
29
+ end}#{override_msg}"
30
+ true
31
+ else
32
+ false
33
+ end
34
+ end
35
+
36
+ def generate_htmlproofer_config _context
37
+ base_config = File.join(CONFIG_VENDOR_DIR, 'htmlproofer.yml')
38
+ project_config = '.config/htmlproofer.local.yml'
39
+ generated_config = CONFIG_PATHS[:htmlproofer]
40
+
41
+ return false unless File.exist?(base_config)
42
+
43
+ merged_content = if File.exist?(project_config)
44
+ merge_yaml_configs(base_config, project_config)
45
+ else
46
+ File.read(base_config)
47
+ end
48
+
49
+ if !File.exist?(generated_config) || File.read(generated_config) != merged_content
50
+ File.write(generated_config, merged_content)
51
+ puts " 📝 Generated #{generated_config} from base#{' + local' if File.exist?(project_config)}"
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ def load_htmlproofer_config config_path=nil, policy: 'merge'
59
+ config_paths = if config_path && File.exist?(config_path)
60
+ [config_path]
61
+ else
62
+ [CONFIG_PATHS[:htmlproofer]]
63
+ end
64
+ config_paths << File.join(CONFIG_VENDOR_DIR, 'htmlproofer.yml') unless policy == 'replace'
65
+ config_path = config_paths.find { |path| File.exist?(path) }
66
+
67
+ return unless config_path
68
+
69
+ puts "📋 Using HTMLProofer config: #{config_path}"
70
+ begin
71
+ config = YAML.load_file(config_path)
72
+ # Convert string patterns to regex for ignore_urls and ignore_files
73
+ process_htmlproofer_patterns(config)
74
+ rescue StandardError => e
75
+ puts "⚠️ Failed to load #{config_path}: #{e.message}"
76
+ end
77
+ end
78
+
79
+ def process_htmlproofer_patterns config
80
+ # Convert string patterns to regex for ignore_urls
81
+ if config['ignore_urls'].is_a?(Array)
82
+ config['ignore_urls'] = config['ignore_urls'].map do |pattern|
83
+ if pattern.is_a?(String) && pattern.start_with?('/') && pattern.end_with?('/')
84
+ Regexp.new(pattern[1..-2])
85
+ else
86
+ pattern
87
+ end
88
+ end
89
+ end
90
+
91
+ # Convert string patterns to regex for ignore_files
92
+ if config['ignore_files'].is_a?(Array)
93
+ config['ignore_files'] = config['ignore_files'].map do |pattern|
94
+ pattern.is_a?(String) ? Regexp.new(pattern) : pattern
95
+ end
96
+ end
97
+
98
+ # Convert string keys to symbols for HTMLProofer
99
+ config.transform_keys(&:to_sym)
100
+ end
101
+
102
+ def merge_yaml_configs base_path, local_path
103
+ # Implement RuboCop-style inheritance for YAML files
104
+ require 'yaml'
105
+
106
+ base_config = YAML.load_file(base_path) || {}
107
+ project_config = YAML.load_file(local_path) || {}
108
+
109
+ # Merge with RuboCop semantics
110
+ merged_config = deep_merge_configs(base_config, project_config)
111
+
112
+ # Convert back to YAML
113
+ YAML.dump(merged_config)
114
+ end
115
+
116
+ def deep_merge_configs base, local
117
+ return local if base.nil?
118
+ return base if local.nil?
119
+
120
+ case local
121
+ when Hash
122
+ result = base.is_a?(Hash) ? base.dup : {}
123
+ local.each do |key, value|
124
+ if value.nil? # YAML null (~) cancels the setting
125
+ result.delete(key)
126
+ else
127
+ result[key] = deep_merge_configs(result[key], value)
128
+ end
129
+ end
130
+ result
131
+ else
132
+ # Non-hash values: local completely overrides base (including arrays)
133
+ local
134
+ end
135
+ end
136
+
137
+ def generate_simple_ini config
138
+ lines = []
139
+
140
+ # Global section first
141
+ if config['global'] && !config['global'].empty?
142
+ config['global'].each do |key, value|
143
+ lines << "#{key} = #{value}"
144
+ end
145
+ lines << ''
146
+ end
147
+
148
+ # Other sections
149
+ config.each do |section_name, section_data|
150
+ next if section_name == 'global'
151
+ next if section_data.empty?
152
+
153
+ lines << "[#{section_name}]"
154
+ section_data.each do |key, value|
155
+ lines << "#{key} = #{value}"
156
+ end
157
+ lines << ''
158
+ end
159
+
160
+ "#{lines.join("\n").strip}\n"
161
+ end
162
+
163
+ def get_path_config tool_slug, context
164
+ tool_meta = context.get_tool_metadata(tool_slug)
165
+ default_config = tool_meta&.dig('paths') || {}
166
+
167
+ manifest = context.load_manifest
168
+ project_config = manifest&.dig('tools')&.find { |t| t['tool'] == tool_slug }&.dig('paths') || {}
169
+
170
+ git_tracked_only = if project_config.key?('git_tracked_only')
171
+ project_config['git_tracked_only']
172
+ else
173
+ default_config.fetch('git_tracked_only', true)
174
+ end
175
+
176
+ # Project-level 'lint'/'skip' overrides gem-level 'patterns'/'ignored_paths'
177
+ lint_paths = project_config['lint'] || default_config['patterns']
178
+ skip_paths = (project_config['skip'] || []) + (default_config['ignored_paths'] || [])
179
+
180
+ {
181
+ lint: lint_paths,
182
+ skip: skip_paths.uniq,
183
+ exts: project_config['exts'] || default_config['exts'],
184
+ git_tracked_only: git_tracked_only
185
+ }
186
+ end
187
+
188
+ def merge_ini_configs base_path, local_path
189
+ # Simple but working INI merger; good enough for our needs
190
+ base_config = parse_simple_ini(File.read(base_path))
191
+ project_config = parse_simple_ini(File.read(local_path))
192
+
193
+ # Merge with RuboCop semantics: local overrides base, sections merge
194
+ merged_config = deep_merge_configs(base_config, project_config)
195
+
196
+ # Convert back to INI format
197
+ generate_simple_ini(merged_config)
198
+ end
199
+
200
+ def parse_simple_ini content
201
+ config = { 'global' => {} }
202
+ current_section = 'global'
203
+
204
+ content.lines.each do |line|
205
+ line = line.strip
206
+ next if line.empty? || line.start_with?('#')
207
+
208
+ if line =~ /^\[(.+)\]$/
209
+ current_section = ::Regexp.last_match(1)
210
+ config[current_section] = {}
211
+ elsif line =~ /^([^=]+)\s*=\s*(.*)$/
212
+ key = ::Regexp.last_match(1).strip
213
+ value = ::Regexp.last_match(2).strip
214
+ config[current_section][key] = value
215
+ end
216
+ end
217
+
218
+ config
219
+ end
220
+
221
+ def apply_vale_style_override content, override_type
222
+ # Parse the INI content
223
+ config = parse_simple_ini(content)
224
+
225
+ # Apply the override to the [*.adoc] section
226
+ if config['*.adoc']
227
+ case override_type
228
+ when :text
229
+ config['*.adoc']['BasedOnStyles'] = 'RedHat, DocOpsLab-Authoring'
230
+ when :adoc
231
+ config['*.adoc']['BasedOnStyles'] = 'AsciiDoc, DocOpsLab-AsciiDoc'
232
+ end
233
+ end
234
+
235
+ # Convert back to INI format
236
+ generate_simple_ini(config)
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module DocOpsLab
6
+ module Dev
7
+ module FileUtilities
8
+ class << self
9
+ def find_shell_scripts context
10
+ # First, try to find files using the new path configuration system
11
+ files = find_files_to_lint('shellcheck', context)
12
+ return files if files && !files.empty?
13
+
14
+ # Fallback to old method if no paths are configured for shellcheck
15
+ scripts = []
16
+ patterns = [
17
+ '**/*.sh',
18
+ '**/*.bash',
19
+ '**/.*rc',
20
+ '**/.*profile',
21
+ 'scripts/*.sh'
22
+ ]
23
+ patterns.each do |pattern|
24
+ Dir.glob(pattern).each do |file|
25
+ next unless File.file?(file)
26
+ next if file.include?('/.vendor/')
27
+ next if file.include?('/node_modules/')
28
+ next unless FileUtilities.git_tracked_or_staged?(file)
29
+
30
+ scripts << file if File.executable?(file) || FileUtilities.shell_shebang?(file)
31
+ end
32
+ end
33
+ scripts.uniq.sort
34
+ end
35
+
36
+ def shell_shebang? file
37
+ return false unless File.readable?(file)
38
+
39
+ first_line = File.open(file, 'r') do |f|
40
+ f.readline
41
+ rescue StandardError
42
+ ''
43
+ end
44
+ first_line.start_with?('#!') &&
45
+ (first_line.include?('sh') || first_line.include?('bash'))
46
+ end
47
+
48
+ def find_files_to_lint tool_slug, context
49
+ path_config = context.get_path_config(tool_slug)
50
+ lint_paths = path_config[:lint]
51
+ skip_paths = path_config[:skip]
52
+ exts = path_config[:exts]
53
+ git_tracked_only = path_config[:git_tracked_only]
54
+
55
+ return [] unless lint_paths
56
+
57
+ files = []
58
+ lint_paths.each do |path|
59
+ # If path is a directory, search recursively. Otherwise, it's a glob.
60
+ glob_pattern = File.directory?(path) ? File.join(path, '**', '*') : path
61
+ Dir.glob(glob_pattern).each do |file|
62
+ next unless File.file?(file)
63
+
64
+ # Normalize path by removing ./ prefix for consistent pattern matching
65
+ normalized = file.sub(%r{^\./}, '')
66
+ files << normalized
67
+ end
68
+ end
69
+
70
+ files.uniq!
71
+
72
+ # Filter by extension if exts is provided
73
+ if exts && !exts.empty?
74
+ files.select! do |file|
75
+ ext = File.extname(file).delete_prefix('.')
76
+ exts.include?(ext)
77
+ end
78
+ end
79
+
80
+ # Filter out ignored paths
81
+ files.reject! do |file|
82
+ should_skip = skip_paths.any? do |ignored|
83
+ FileUtilities.file_matches_ignore_pattern?(file, ignored)
84
+ end
85
+ should_skip
86
+ end
87
+
88
+ # Filter by git tracking status
89
+ if git_tracked_only
90
+ files.select! do |file|
91
+ is_tracked = FileUtilities.git_tracked_or_staged?(file)
92
+ is_tracked
93
+ end
94
+ end
95
+
96
+ files.sort
97
+ end
98
+
99
+ def find_asciidoc_files context
100
+ FileUtilities.find_files_to_lint('vale', context)
101
+ end
102
+
103
+ def file_matches_ignore_pattern? file, pattern
104
+ if pattern.include?('*') || pattern.include?('?')
105
+ # Handle glob patterns
106
+ # If pattern ends with /*, treat it as recursive (dir/**/*)
107
+ recursive_pattern = if pattern.end_with?('/*')
108
+ pattern.sub(%r{/\*$}, '/**/*')
109
+ else
110
+ pattern
111
+ end
112
+
113
+ File.fnmatch(recursive_pattern, file, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
114
+ # Also try exact match without modification for explicit patterns
115
+ File.fnmatch(pattern, file, File::FNM_PATHNAME | File::FNM_DOTMATCH)
116
+ else
117
+ # Non-glob patterns: match directory name anywhere in path
118
+ File.fnmatch("**/#{pattern}/**", file, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
119
+ File.fnmatch("**/#{pattern}", file, File::FNM_PATHNAME | File::FNM_DOTMATCH)
120
+ end
121
+ end
122
+
123
+ def git_tracked_or_staged? file
124
+ return true unless Dir.exist?('.git')
125
+
126
+ repo_root = `git rev-parse --show-toplevel`.strip
127
+ rel = Pathname.new(file).expand_path.relative_path_from(Pathname.new(repo_root)).to_s
128
+
129
+ # Check if the file is tracked
130
+ return true if system('git', 'ls-files', '--error-unmatch', rel, out: File::NULL, err: File::NULL)
131
+
132
+ # Check if the file is staged (but not necessarily committed yet)
133
+ return true if system('git', 'diff', '--name-only', '--cached', '--', rel, out: File::NULL, err: File::NULL)
134
+
135
+ false
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module DocOpsLab
6
+ module Dev
7
+ module GitHooks
8
+ class << self
9
+ def install_missing_hooks
10
+ return unless Dir.exist?(hooks_dir)
11
+
12
+ Dir.glob("#{hooks_template_dir}/*.sh").each do |template_path|
13
+ hook_name = File.basename(template_path, '.sh')
14
+ hook_path = File.join(hooks_dir, hook_name)
15
+
16
+ next if File.exist?(hook_path)
17
+
18
+ puts "🪝 Installing #{hook_name} hook..."
19
+ FileUtils.cp(template_path, hook_path)
20
+ File.chmod(0o755, hook_path)
21
+ puts "✅ #{hook_name} hook installed"
22
+ end
23
+ end
24
+
25
+ def check_hook_updates
26
+ return puts 'ℹ️ No .git directory found' unless Dir.exist?(hooks_dir)
27
+
28
+ updates_available = false
29
+
30
+ Dir.glob("#{hooks_template_dir}/*.sh").each do |template_path|
31
+ hook_name = File.basename(template_path, '.sh')
32
+ hook_path = File.join(hooks_dir, hook_name)
33
+
34
+ if File.exist?(hook_path)
35
+ template_content = File.read(template_path)
36
+ current_content = File.read(hook_path)
37
+
38
+ if template_content != current_content
39
+ puts "🔄 Update available for #{hook_name} hook"
40
+ updates_available = true
41
+ end
42
+ else
43
+ puts "➕ New hook template available: #{hook_name}"
44
+ updates_available = true
45
+ end
46
+ end
47
+
48
+ if updates_available
49
+ puts "Run 'rake labdev:sync:hooks' to update hooks interactively"
50
+ else
51
+ puts '✅ All hooks are up to date'
52
+ end
53
+ end
54
+
55
+ def update_hooks_interactive
56
+ return puts 'ℹ️ No .git directory found' unless Dir.exist?(hooks_dir)
57
+
58
+ Dir.glob("#{hooks_template_dir}/*.sh").each do |template_path|
59
+ hook_name = File.basename(template_path, '.sh')
60
+ hook_path = File.join(hooks_dir, hook_name)
61
+
62
+ if File.exist?(hook_path)
63
+ template_content = File.read(template_path)
64
+ current_content = File.read(hook_path)
65
+
66
+ next if template_content == current_content
67
+
68
+ puts "🔄 #{hook_name} hook has updates available"
69
+ puts "Current file exists at: #{hook_path}"
70
+
71
+ print "Update #{hook_name} hook? [y/N]: "
72
+ response = $stdin.gets.chomp.downcase
73
+
74
+ if %w[y yes].include?(response)
75
+ File.write(hook_path, template_content)
76
+ File.chmod(0o755, hook_path)
77
+ puts "✅ #{hook_name} hook updated"
78
+ else
79
+ puts "⏭️ Skipped #{hook_name} hook"
80
+ end
81
+ else
82
+ puts "➕ New hook template: #{hook_name}"
83
+ print "Install #{hook_name} hook? [Y/n]: "
84
+ response = $stdin.gets.chomp.downcase
85
+
86
+ if response != 'n' && response != 'no'
87
+ FileUtils.cp(template_path, hook_path)
88
+ File.chmod(0o755, hook_path)
89
+ puts "✅ #{hook_name} hook installed"
90
+ else
91
+ puts "⏭️ Skipped #{hook_name} hook"
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def list_hook_templates
98
+ puts '📋 Available git hook templates:'
99
+ puts ''
100
+
101
+ Dir.glob("#{hooks_template_dir}/*.sh").each do |template_path|
102
+ hook_name = File.basename(template_path, '.sh')
103
+ hook_path = File.join(hooks_dir, hook_name)
104
+
105
+ status = nil
106
+ if File.exist?(hook_path)
107
+ template_content = File.read(template_path)
108
+ current_content = File.read(hook_path)
109
+ status = template_content == current_content ? '✅ installed' : '🔄 update available'
110
+ else
111
+ status = '➕ not installed'
112
+ end
113
+
114
+ description = case hook_name
115
+ when 'pre-commit'
116
+ 'Advisory checks & syntax validation (non-blocking)'
117
+ when 'pre-push'
118
+ 'Comprehensive linting & quality gate (blocking)'
119
+ else
120
+ ''
121
+ end
122
+
123
+ puts " #{hook_name}: #{status}"
124
+ puts " #{description}" unless description.empty?
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def hooks_template_dir
131
+ HOOKS_SOURCE_DIR
132
+ end
133
+
134
+ def hooks_dir
135
+ HOOKS_DIR
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+
6
+ module DocOpsLab
7
+ module Dev
8
+ module Help
9
+ class << self
10
+ def show_task_help task_string=nil
11
+ tasks_def_path = File.join(GEM_ROOT, 'specs/data/tasks-def.yml')
12
+
13
+ unless File.exist?(tasks_def_path)
14
+ puts '❌ Tasks definition file not found'
15
+ return
16
+ end
17
+
18
+ tasks_def = YAML.load_file(tasks_def_path)
19
+
20
+ if task_string.nil?
21
+ show_general_help
22
+ return
23
+ end
24
+
25
+ # Normalize task string (allow with or without labdev: prefix)
26
+ task_string = "labdev:#{task_string}" unless task_string.start_with?('labdev:')
27
+
28
+ # Parse task path
29
+ task_parts = task_string.sub('labdev:', '').split(':')
30
+
31
+ # Navigate to task in YAML structure
32
+ current = tasks_def['labdev']
33
+ found = true
34
+ task_parts.each do |part|
35
+ if current.is_a?(Hash) && current[part]
36
+ current = current[part]
37
+ else
38
+ puts "❌ Task not found: #{task_string}"
39
+ found = false
40
+ break
41
+ end
42
+ end
43
+
44
+ return unless found
45
+
46
+ show_task_details(task_string, current)
47
+ end
48
+
49
+ private
50
+
51
+ def show_general_help
52
+ puts '📚 DocOps Lab Development Tools - Available Tasks'
53
+ puts '=' * 60
54
+ puts ''
55
+ puts 'Use `bundle exec rake -T | grep labdev:` to see all tasks.'
56
+ puts 'Use `bundle exec rake labdev:help[verb:subtask]` for detailed help.'
57
+ puts ''
58
+ puts "Example: bundle exec rake 'labdev:help[run:script]'"
59
+ puts ''
60
+ end
61
+
62
+ def show_task_details task_string, task_info
63
+ puts "📚 Help for: #{task_string}"
64
+ puts '=' * 60
65
+ puts ''
66
+
67
+ if task_info['_desc']
68
+ puts "Description: #{task_info['_desc']}"
69
+ puts ''
70
+ end
71
+
72
+ if task_info['_docs']
73
+ puts 'Documentation:'
74
+ puts task_info['_docs']
75
+ puts ''
76
+ end
77
+
78
+ if task_info['_alias']
79
+ puts "⚠️ This is an alias for: #{task_info['_alias']}"
80
+ # Display help for the aliased task
81
+ show_task_help(task_info['_alias'])
82
+ return
83
+ end
84
+
85
+ # Show subtasks if this is a namespace
86
+ subtasks = task_info.select { |k, v| !k.start_with?('_') && v.is_a?(Hash) }
87
+ if subtasks.any?
88
+ puts 'Available subtasks:'
89
+ puts ''
90
+ subtasks.each do |subtask_name, subtask_info|
91
+ desc = subtask_info['_desc'] || '(no description)'
92
+ alias_note = subtask_info['_alias'] ? " → #{subtask_info['_alias']}" : ''
93
+ puts " #{task_string}:#{subtask_name}#{alias_note}"
94
+ puts " #{desc}"
95
+ puts ''
96
+ end
97
+ end
98
+
99
+ if task_info['_args']
100
+ puts 'Arguments:'
101
+ task_info['_args'].each do |arg_name, arg_info|
102
+ required = arg_info['required'] ? '(required)' : '(optional)'
103
+ puts " #{arg_name} #{required}"
104
+ puts " #{arg_info['summ']}" if arg_info['summ']
105
+ puts " #{arg_info['docs'].strip.split("\n").join("\n ")}" if arg_info['docs']
106
+ puts ''
107
+ end
108
+ end
109
+
110
+ return unless task_info['_test']
111
+
112
+ puts 'Example usage:'
113
+ task_info['_test'].each do |test_cmd|
114
+ puts " #{test_cmd}"
115
+ end
116
+ puts ''
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end