proofmode 0.9.0

Capture, share, and preserve verifiable photos and videos
Documentation
#!/usr/bin/env ruby

require 'optparse'
require 'json'
require 'pathname'

# Add lib to load path
lib = File.expand_path('../lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)

require 'proofmode'

module ProofMode
  class CLI
    def self.start(args = ARGV)
      new.run(args)
    end

    def run(args)
      options = {}
      parser = create_parser(options)
      
      begin
        parser.parse!(args)
      rescue OptionParser::InvalidOption => e
        puts "Error: #{e.message}"
        puts parser
        exit 1
      end

      command = args.shift

      case command
      when 'generate'
        generate_proof(options)
      when 'check'
        check_files(options)
      when 'version'
        show_version
      when nil
        puts parser
        exit 1
      else
        puts "Unknown command: #{command}"
        puts parser
        exit 1
      end
    rescue StandardError => e
      puts "Error: #{e.message}"
      puts e.backtrace if options[:debug]
      exit 1
    end

    private

    def create_parser(options)
      OptionParser.new do |opts|
        opts.banner = "Usage: proofmode [command] [options]"
        opts.separator ""
        opts.separator "Commands:"
        opts.separator "  generate    Generate cryptographic proof for media files"
        opts.separator "  check       Verify media files"
        opts.separator "  version     Show version information"
        opts.separator ""
        opts.separator "Options:"

        opts.on("-f", "--file FILE", "Input file") do |f|
          options[:files] ||= []
          options[:files] << f
        end

        opts.on("-d", "--dir DIRECTORY", "Input directory") do |d|
          options[:dirs] ||= []
          options[:dirs] << d
        end

        opts.on("-s", "--storage PATH", "Storage directory (default: ./proofmode)") do |s|
          options[:storage] = s
        end

        opts.on("-e", "--email EMAIL", "Email for PGP key") do |e|
          options[:email] = e
        end

        opts.on("-p", "--passphrase PASS", "Passphrase for PGP key") do |p|
          options[:passphrase] = p
        end

        opts.on("-o", "--output FILE", "Output file for results (JSON)") do |o|
          options[:output] = o
        end

        opts.on("--json", "Output results as JSON") do
          options[:json] = true
        end

        opts.on("--debug", "Show debug information") do
          options[:debug] = true
        end

        opts.on("-h", "--help", "Show this help message") do
          puts opts
          exit
        end
      end
    end

    def generate_proof(options)
      storage_path = options[:storage] || "./proofmode"
      email = options[:email] || "user@example.com"
      passphrase = options[:passphrase] || "default_passphrase"

      files = collect_files(options)
      if files.empty?
        puts "Error: No input files specified"
        exit 1
      end

      generator = ProofMode::Generator.new(
        storage_path: storage_path,
        email: email,
        passphrase: passphrase
      )

      results = []
      files.each do |file|
        unless File.exist?(file)
          puts "Warning: File not found: #{file}"
          next
        end

        puts "Generating proof for: #{file}" unless options[:json]
        
        begin
          hash = generator.generate_from_file(file)
          result = { file: file, hash: hash, status: 'success' }
          results << result
          
          unless options[:json]
            puts "  Hash: #{hash}"
            puts "  Proof saved to: #{File.join(storage_path, hash[0..1], hash[2..3], hash)}"
          end
        rescue => e
          result = { file: file, error: e.message, status: 'error' }
          results << result
          puts "  Error: #{e.message}" unless options[:json]
        end
      end

      if options[:json] || options[:output]
        output = JSON.pretty_generate({
          command: 'generate',
          results: results,
          storage_path: storage_path
        })

        if options[:output]
          File.write(options[:output], output)
          puts "Results written to: #{options[:output]}" unless options[:json]
        else
          puts output
        end
      end
    end

    def check_files(options)
      files = collect_files(options)
      if files.empty?
        puts "Error: No input files specified"
        exit 1
      end

      checker = ProofMode::Checker.new

      begin
        result_json = checker.check_files(files)
        result = JSON.parse(result_json)

        if options[:json]
          puts JSON.pretty_generate(result)
        elsif options[:output]
          File.write(options[:output], JSON.pretty_generate(result))
          puts "Results written to: #{options[:output]}"
        else
          display_check_results(result)
        end
      rescue => e
        puts "Error checking files: #{e.message}"
        exit 1
      end
    end

    def display_check_results(result)
      puts "\nVerification Results"
      puts "===================="
      
      result['files'].each do |file_result|
        puts "\nFile: #{file_result['path']}"
        
        if file_result['error']
          puts "  Error: #{file_result['error']}"
        else
          ver = file_result['verification_results']
          
          # C2PA
          if ver['c2pa']
            status = ver['c2pa']['found'] ? '✓ Found' : '✗ Not found'
            puts "  C2PA: #{status}"
          end
          
          # PGP
          if ver['pgp']
            status = ver['pgp']['found'] ? '✓ Found' : '✗ Not found'
            puts "  PGP: #{status}"
          end
          
          # OpenTimestamps
          if ver['ots']
            status = ver['ots']['found'] ? '✓ Found' : '✗ Not found'
            puts "  OpenTimestamps: #{status}"
          end
          
          # EXIF
          if ver['exif']
            status = ver['exif']['found'] ? '✓ Found' : '✗ Not found'
            puts "  EXIF: #{status}"
            if ver['exif']['found'] && ver['exif']['data']
              puts "    Camera: #{ver['exif']['data']['make']} #{ver['exif']['data']['model']}" if ver['exif']['data']['make']
              puts "    Date: #{ver['exif']['data']['date_time']}" if ver['exif']['data']['date_time']
            end
          end
        end
      end
      
      # Summary
      total = result['files'].length
      errors = result['files'].count { |f| f['error'] }
      puts "\nSummary: #{total} files checked, #{errors} errors"
    end

    def show_version
      puts "ProofMode Ruby CLI v#{ProofMode::VERSION}"
      puts "Using: #{ProofMode.implementation_type}"
    end

    def collect_files(options)
      files = options[:files] || []
      
      if options[:dirs]
        options[:dirs].each do |dir|
          if Dir.exist?(dir)
            Dir.glob(File.join(dir, '*')).each do |path|
              files << path if File.file?(path)
            end
          else
            puts "Warning: Directory not found: #{dir}"
          end
        end
      end
      
      files.uniq
    end
  end
end

# Run the CLI
ProofMode::CLI.start if __FILE__ == $0