Class: Zwischen::CLI

Inherits:
Thor
  • Object
show all
Defined in:
lib/zwischen/cli.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean

Disable Thor’s pager to prevent help from hanging

Returns:

  • (Boolean)


11
12
13
# File 'lib/zwischen/cli.rb', line 11

def self.exit_on_failure?
  true
end

Instance Method Details

#doctorObject



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/zwischen/cli.rb', line 27

def doctor
  installer = Installer.new
  puts "\n" + "=" * 60
  puts "Zwischen Doctor - Tool Status".colorize(:bold)
  puts "=" * 60 + "\n"

  tools = {
    "gitleaks" => "Secrets detection",
    "semgrep" => "Static analysis"
  }

  all_installed = true

  tools.each do |tool_name, description|
    # Check both ~/.zwischen/bin/ and system PATH
    local_path = File.join(File.expand_path("~/.zwischen/bin"), tool_name)
    installed = File.executable?(local_path) || installer.check_tool(tool_name)

    if installed
      # Get version from the correct path
      executable = File.executable?(local_path) ? local_path : tool_name
      version = begin
        `#{executable} --version 2>/dev/null`.strip.split("\n").first
      rescue
        nil
      end

      puts "#{tool_name}".colorize(:green) + " - #{description}"
      puts "  Version: #{version}" if version && !version.empty?
      puts "  Location: #{executable}" if File.executable?(local_path)
    else
      all_installed = false
      puts "#{tool_name}".colorize(:red) + " - #{description} - NOT FOUND"
      puts "#{installer.preferred_command(tool_name)}"
    end
    puts ""
  end

  if all_installed
    puts "✅ All tools are installed and ready!".colorize(:green)
  else
    puts "⚠️  Some tools are missing. Install them using the commands above.".colorize(:yellow)
  end

  puts ""
end

#help(command = nil, subcommand = false) ⇒ Object

Disable pager for help output



16
17
18
19
# File 'lib/zwischen/cli.rb', line 16

def help(command = nil, subcommand = false)
  ENV["THOR_PAGER"] = "cat" if ENV["THOR_PAGER"].nil?
  super
end

#initObject



22
23
24
# File 'lib/zwischen/cli.rb', line 22

def init
  Setup.run
end

#scanObject



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/zwischen/cli.rb', line 81

def scan
  config = Config.load
  project = ProjectDetector.detect
  pre_push = options[:"pre-push"]
  quiet = pre_push || %w[json sarif].include?(options[:format])

  # Suppress scanning message in pre-push/machine-readable modes
  unless quiet
    puts "🔍 Scanning #{project[:primary_type] || 'project'}...\n"
  end

  changed_files = nil
  if pre_push || options[:changed]
    changed_files = GitDiff.changed_files(include_working_tree: !pre_push)
    changed_files = changed_files.select do |path|
      candidate = path
      candidate = File.join(project[:root], candidate) unless Pathname.new(candidate).absolute?
      File.file?(candidate)
    end

    if changed_files.empty?
      puts Reporter::Sarif.report({ findings: [] }, project_root: project[:root]) if options[:format] == "sarif"
      exit 0
    end
  end

  # Run scanners
  orchestrator = Scanner::Orchestrator.new(config: config)
  findings = orchestrator.scan(project[:root], only: options[:only], pre_push: pre_push, files: changed_files)

  # Filter findings to changed files in pre-push/--changed mode
  # Note: This is a safety net. Scanners receive the file list and should only scan those,
  # but some scanners (like gitleaks) may return paths in different formats. This ensures
  # we only report findings for files the developer actually changed.
  if changed_files
    findings = GitDiff.filter_findings(findings: findings, changed_files: changed_files)
  end

  if findings.empty?
    # In pre-push mode, exit silently (no output)
    if options[:format] == "sarif"
      puts Reporter::Sarif.report({ findings: [] }, project_root: project[:root])
    elsif !quiet
      puts "✅ No issues found.".colorize(:green)
    end
    exit 0
  end

  # Aggregate findings
  aggregated = Finding::Aggregator.aggregate(findings)

  # Determine AI provider
  provider = if options[:ai] && !options[:ai].empty? && options[:ai] != "true"
               options[:ai]
             else
               config.ai_provider
             end

  # AI analysis if enabled
  ai_enabled = if pre_push
    # In pre-push mode, use config to determine AI
    config.ai_pre_push_enabled?
  else
    # Manual scan: use flag or config
    !options[:ai].nil? || config.ai_enabled?
  end

  if ai_enabled
    begin
      unless quiet
        puts "🤖 Analyzing findings with AI (#{provider})...\n"
      end
      
      api_key = options[:"api-key"] || config.ai_api_key(provider)
      provider_config = config.ai_provider_config(provider)

      analyzer = AI::Analyzer.new(
        provider: provider,
        api_key: api_key,
        config: provider_config,
        project_context: project
      )
      enhanced_findings = analyzer.analyze(aggregated[:findings])
      aggregated = Finding::Aggregator.aggregate(enhanced_findings)
    rescue AI::Error => e
      warn "⚠️  AI analysis unavailable: #{e.message}" unless pre_push
      # In pre-push mode, continue silently without AI
    end
  end

  # Report results
  if options[:format] == "json"
    require "json"
    puts JSON.pretty_generate({
      summary: aggregated[:summary],
      findings: aggregated[:findings].map { |f| f.to_h.merge(file: relative_path(f.file, project[:root])) }
    })
    blocking_severity = config.blocking_severity
    exit_code = aggregated[:findings].any? { |f| should_block?(f, blocking_severity, ai_enabled) } ? 1 : 0
    exit exit_code
  elsif options[:format] == "sarif"
    puts Reporter::Sarif.report(aggregated, project_root: project[:root])
    blocking_severity = config.blocking_severity
    exit_code = aggregated[:findings].any? { |f| should_block?(f, blocking_severity, ai_enabled) } ? 1 : 0
    exit exit_code
  else
    if pre_push
      exit_code = Reporter::Terminal.report_compact(aggregated, config: config, ai_enabled: ai_enabled)
    else
      exit_code = Reporter::Terminal.report(aggregated, config: config, ai_enabled: ai_enabled)
    end
    exit exit_code
  end
rescue StandardError => e
  puts "❌ Error: #{e.message}".colorize(:red)
  puts e.backtrace if ENV["DEBUG"]
  exit 1
end

#uninstallObject



201
202
203
# File 'lib/zwischen/cli.rb', line 201

def uninstall
  Setup.uninstall
end