github-daily-digest 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5f607d1a7a04aa2735c14f7d6872c348efa10aa74c7a91296a35fd39e6705be8
4
+ data.tar.gz: b4d1d4e1de8ede127a6844919736afb6f5805ea29b6a8e2606bba3371c18bb92
5
+ SHA512:
6
+ metadata.gz: cf2f46d7f565b40c1731e1386a6eaa394947be98168befd7f59dc805ed0f7864bf01d34bb7e38be47fdb9b99f7a8b6e174fbd119eb768540c4ec1072b3105608
7
+ data.tar.gz: 2ee9c02d0ccab93af5870ecee1525fdd4140f1b9723a0bfd231f3af4e18117db77477553062b17ad281debfbcbdee6fd227785d47dd5735f9c27da4b1dfe3aeb
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # GithubDailyDigest
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/github_daily_digest`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/github_daily_digest.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "github-daily-digest"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This script uses the github_daily_digest gem to run the daily analysis.
4
+
5
+ # --- Optional: Load .env for convenience if dotenv gem is available ---
6
+ # This allows users to manage secrets via .env when running the executable,
7
+ # but the core gem library doesn't depend on it.
8
+ begin
9
+ require 'dotenv/load'
10
+ # Don't print this in json-only mode
11
+ rescue LoadError
12
+ # dotenv gem not found, proceed using environment variables set externally.
13
+ end
14
+ # --------------------------------------------------------------------
15
+
16
+ # Add the lib directory to the load path for running from the repository
17
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
18
+
19
+ require 'github-daily-digest' # Load the gem's code from lib/
20
+ require 'logger'
21
+ require 'json'
22
+ require 'fileutils'
23
+
24
+ begin
25
+ # Load Configuration with command-line arguments
26
+ config = GithubDailyDigest::Configuration.new(ARGV)
27
+
28
+ # Show help and exit if requested
29
+ if config.help_requested
30
+ puts config.help_text
31
+ exit(0)
32
+ end
33
+
34
+ # Setup Logger based on configuration
35
+ logger = if config.output_to_stdout
36
+ # Use STDOUT for direct output
37
+ logger = Logger.new(STDOUT)
38
+ # Format logs without timestamps when outputting to terminal
39
+ logger.formatter = proc do |severity, datetime, progname, msg|
40
+ config.json_only ? "" : "[#{severity.ljust(5)}]: #{msg}\n"
41
+ end
42
+ logger
43
+ else
44
+ # Traditional log file if explicitly requested
45
+ log_file_path = File.expand_path('daily_digest.log', Dir.pwd)
46
+ logger = Logger.new(log_file_path, 'daily', 7)
47
+ logger.formatter = proc do |severity, datetime, progname, msg|
48
+ "#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity.ljust(5)}]: #{msg}\n"
49
+ end
50
+ puts "Logging to: #{log_file_path}"
51
+ logger
52
+ end
53
+
54
+ logger.level = Logger.const_get(config.log_level)
55
+
56
+ # Only output this in verbose mode
57
+ logger.info("Starting GitHub Daily Digest") unless config.json_only
58
+
59
+ rescue ArgumentError => e
60
+ # Configuration errors (missing ENV vars, invalid formats)
61
+ if config&.json_only
62
+ puts JSON.pretty_generate({ error: e.message })
63
+ else
64
+ puts "Error: #{e.message}"
65
+ puts "Use --help for usage information"
66
+ end
67
+ exit(1)
68
+ rescue => e
69
+ # Catch other potential setup errors
70
+ if config&.json_only
71
+ puts JSON.pretty_generate({ error: e.message })
72
+ else
73
+ puts "Initialization Error: #{e.message}"
74
+ puts e.backtrace.join("\n")
75
+ end
76
+ exit(1)
77
+ end
78
+
79
+ # --- Main Execution ---
80
+ begin
81
+ # Only output this in verbose mode
82
+ logger.info("Results will be output directly") unless config.json_only
83
+
84
+ # Create the runner with our configuration
85
+ runner = GithubDailyDigest::DailyDigestRunner.new(config: config, logger: logger)
86
+
87
+ # Run and get the results
88
+ results = runner.run
89
+
90
+ # Output results in proper format and destination
91
+ output_formatter = GithubDailyDigest::OutputFormatter.new(config: config, logger: logger)
92
+ formatted_output = output_formatter.format(results)
93
+
94
+ # Send to correct destination
95
+ if config.output_to_stdout
96
+ # Just output to stdout
97
+ puts formatted_output
98
+ else
99
+ # Save to a log file
100
+ results_dir = File.expand_path('results', Dir.pwd)
101
+ FileUtils.mkdir_p(results_dir)
102
+
103
+ # Create appropriate filename with format extension
104
+ format_ext = config.output_format == 'markdown' ? 'md' : 'json'
105
+ results_file = File.join(results_dir, "daily_digest_#{Time.now.strftime('%Y%m%d_%H%M%S')}.#{format_ext}")
106
+ File.write(results_file, formatted_output)
107
+ logger.info("Results saved to #{results_file}")
108
+ end
109
+
110
+ logger.info("Execution finished successfully.") unless config.json_only
111
+
112
+ exit(0)
113
+ rescue => e
114
+ # Handle errors during execution
115
+ if config&.output_formats&.include?('markdown')
116
+ error_output = "# Error\n\n"
117
+ error_output << "**Error Message:** #{e.message}\n\n"
118
+ error_output << "**Stack Trace:**\n\n```\n#{e.backtrace.join("\n")}\n```\n" unless config&.json_only
119
+
120
+ if config&.output_to_stdout
121
+ puts error_output
122
+ else
123
+ # Try to write to a log file
124
+ begin
125
+ results_dir = File.expand_path('results', Dir.pwd)
126
+ FileUtils.mkdir_p(results_dir)
127
+ results_file = File.join(results_dir, "error_#{Time.now.strftime('%Y%m%d_%H%M%S')}.md")
128
+ File.write(results_file, error_output)
129
+ puts "Error details saved to #{results_file}"
130
+ rescue
131
+ puts error_output
132
+ end
133
+ end
134
+ else # Default to JSON
135
+ error_json = { error: e.message }
136
+ error_json[:backtrace] = e.backtrace unless config&.json_only
137
+ puts JSON.pretty_generate(error_json)
138
+ end
139
+ exit(1)
140
+ end
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,47 @@
1
+ # github_daily_digest/github-daily-digest.gemspec
2
+ require_relative "lib/github-daily-digest/version"
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "github-daily-digest"
6
+ spec.version = GithubDailyDigest::VERSION
7
+ spec.authors = ["Arturas Piksrys"]
8
+ spec.email = ["arturas@wisemonks.com"]
9
+
10
+ spec.summary = "Generates daily activity digests for GitHub organization members using Gemini."
11
+ spec.description = "Fetches recent GitHub commits and PR reviews for organization members and uses Google's Gemini API to analyze and summarize the activity. Provides an executable for daily runs."
12
+ spec.homepage = "https://github.com/wisemonks/github-daily-digest"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.7.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage # Assumes source is at homepage
18
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." # Optional
19
+
20
+ # Specify which files should be added to the gem when it is packaged.
21
+ # Note: Excludes test files, .env, logs, results, etc.
22
+ spec.files = Dir[
23
+ 'lib/**/*',
24
+ 'bin/*',
25
+ 'README.md',
26
+ 'LICENSE',
27
+ 'Rakefile',
28
+ 'github-daily-digest.gemspec',
29
+ 'github-daily-digest.rb'
30
+ ]
31
+ spec.bindir = "bin" # Directory for executables
32
+ spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) } # Find executables in bin/
33
+ spec.require_paths = ["lib"] # Code is loaded from the lib directory
34
+
35
+ # Runtime Dependencies: Gems required for the gem to function
36
+ spec.add_dependency "activesupport", "~> 7.0" # Or your preferred version constraint
37
+ spec.add_dependency "gemini-ai", "~> 4.2"
38
+ spec.add_dependency "logger" # Standard lib, but good practice to list
39
+ spec.add_dependency "octokit", "~> 6.1"
40
+ spec.add_dependency "graphql-client", "~> 0.19.0"
41
+
42
+ # Development Dependencies: Gems needed for development/testing (managed by Gemfile)
43
+ spec.add_development_dependency "bundler", ">= 2.0"
44
+ spec.add_development_dependency "rake", "~> 13.0"
45
+ # spec.add_development_dependency "rspec", "~> 3.0" # If you add tests
46
+ spec.add_development_dependency "dotenv", "~> 2.8" # Needed by the *executable* for convenience
47
+ end
@@ -0,0 +1,20 @@
1
+ # github_daily_digest/lib/github_daily_digest.rb
2
+ require "logger"
3
+ require "active_support/all"
4
+ require "octokit"
5
+ require "gemini-ai"
6
+ require "json"
7
+ require "time"
8
+
9
+ # Order matters for relative requires if classes depend on each other during load
10
+ require_relative "lib/github_daily_digest/version"
11
+ require_relative "lib/configuration"
12
+ require_relative "lib/github_service"
13
+ require_relative "lib/github_graphql_service"
14
+ require_relative "lib/gemini_service"
15
+ require_relative "lib/activity_analyzer"
16
+ require_relative "lib/daily_digest_runner"
17
+
18
+ module GithubDailyDigest
19
+ class Error < StandardError; end
20
+ end
@@ -0,0 +1,48 @@
1
+ # github_daily_digest/lib/activity_analyzer.rb
2
+ require_relative './language_analyzer.rb'
3
+ module GithubDailyDigest
4
+ class ActivityAnalyzer
5
+ def initialize(gemini_service:, github_graphql_service:, logger:)
6
+ @gemini_service = gemini_service
7
+ @github_graphql_service = github_graphql_service
8
+ @logger = logger
9
+ end
10
+
11
+ def analyze(username:, activity_data:, time_window_days: 7)
12
+ @logger.info("Analyzing activity for user: #{username}")
13
+ commits = activity_data[:commits]
14
+ review_count = activity_data[:review_count]
15
+
16
+ commits_with_code = if @github_graphql_service
17
+ @github_graphql_service.fetch_commits_changes(commits)
18
+ else
19
+ @logger.debug("GraphQL service not available, proceeding without detailed commit changes")
20
+ commits
21
+ end
22
+
23
+ analysis_result = @gemini_service.analyze_activity(
24
+ username: username,
25
+ commits_with_code: commits_with_code,
26
+ review_count: review_count,
27
+ time_window_days: time_window_days
28
+ )
29
+ @logger.debug("Gemini analysis result for #{username}: #{analysis_result}")
30
+
31
+ # Add username and timestamp to the result for context
32
+ analysis_result[:username] = username
33
+ analysis_result[:analysis_timestamp] = Time.now.iso8601
34
+
35
+ # Calculate language distribution
36
+
37
+ analysis_result[:language_distribution] = LanguageAnalyzer.calculate_distribution(commits_with_code.map{ |f| f[:code_changes][:files] }.flatten)
38
+
39
+ if analysis_result[:error]
40
+ @logger.error("Analysis failed for #{username}: #{analysis_result[:error]}")
41
+ else
42
+ @logger.info("Analysis successful for #{username}")
43
+ end
44
+
45
+ analysis_result
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,260 @@
1
+ # github_daily_digest/lib/configuration.rb
2
+ require 'dotenv'
3
+ require 'active_support/all' # For parsing duration string
4
+ require 'optparse'
5
+
6
+ module GithubDailyDigest
7
+ class Configuration
8
+ attr_reader :github_token, :gemini_api_key, :github_org_name, :github_org_names,
9
+ :log_level, :fetch_window_duration, :max_api_retries,
10
+ :rate_limit_sleep_base, :time_since, :gemini_model,
11
+ :json_only, :output_to_stdout, :help_requested,
12
+ :output_formats, :output_destination, :concise_output,
13
+ :use_graphql, :no_graphql, :specific_users,
14
+ :html_theme, :html_title, :html_show_charts,
15
+ :time_window_days
16
+
17
+ def initialize(args = nil)
18
+ Dotenv.load
19
+ @json_only = true # Default to JSON only output
20
+ @output_to_stdout = true # Default to stdout
21
+ @gemini_model = ENV.fetch('GEMINI_MODEL', 'gemini-1.5-flash')
22
+ @help_requested = false
23
+
24
+ # Support multiple output formats - if env specifies a single format, convert to array
25
+ output_format_env = ENV.fetch('OUTPUT_FORMAT', 'json').downcase
26
+ @output_formats = output_format_env.include?(',') ?
27
+ output_format_env.split(',').map(&:strip).map(&:downcase) :
28
+ [output_format_env.downcase]
29
+
30
+ @output_destination = ENV.fetch('OUTPUT_DESTINATION', 'stdout').downcase # 'stdout' or 'log'
31
+ @concise_output = ENV.fetch('CONCISE_OUTPUT', 'true').downcase == 'true' # Whether to use concise output format (defaults to true)
32
+ @use_graphql = ENV.fetch('USE_GRAPHQL', 'true').downcase == 'true' # Whether to use GraphQL API (defaults to true)
33
+ @no_graphql = !@use_graphql # Inverse of use_graphql for easier API
34
+ @specific_users = ENV.fetch('SPECIFIC_USERS', '').split(',').map(&:strip).reject(&:empty?) # Specific users to process
35
+
36
+ # HTML-specific options
37
+ @html_theme = ENV.fetch('HTML_THEME', 'default').downcase # 'default', 'dark', or 'light'
38
+ @html_title = ENV.fetch('HTML_TITLE', nil) # Custom title for HTML output
39
+ @html_show_charts = ENV.fetch('HTML_SHOW_CHARTS', 'true').downcase == 'true' # Whether to show charts in HTML output
40
+
41
+ # Parse command line arguments if provided
42
+ parse_command_line_args(args) if args
43
+
44
+ # Early return if help is requested
45
+ return if @help_requested
46
+
47
+ # Load environment variables
48
+ load_env_vars
49
+
50
+ # Validate the configuration
51
+ validate_config
52
+
53
+ # Calculate the time window
54
+ calculate_time_window
55
+ end
56
+
57
+ def help_text
58
+ <<~HELP
59
+ USAGE: github-daily-digest [options]
60
+
61
+ GitHub Daily Digest generates insights about developer activity using GitHub API and Google's Gemini AI.
62
+
63
+ Options:
64
+ -h, --help Show this help message
65
+ -t, --token TOKEN GitHub API token (instead of GITHUB_TOKEN env var)
66
+ -g, --gemini-key KEY Gemini API key (instead of GEMINI_API_KEY env var)
67
+ -o, --org NAME GitHub organization name(s) (instead of GITHUB_ORG_NAME env var)
68
+ Multiple organizations can be comma-separated (e.g., 'org1,org2,org3')
69
+ -u, --users USERNAMES Specific users to process (comma-separated, e.g. 'user1,user2,user3')
70
+ If not specified, all organization members will be processed
71
+ -m, --model MODEL Gemini model to use (default: gemini-1.5-flash)
72
+ -w, --window DURATION Time window for fetching data (e.g., '1.day', '12.hours')
73
+ -v, --verbose Enable verbose output (instead of JSON-only)
74
+ -l, --log-level LEVEL Set log level (DEBUG, INFO, WARN, ERROR, FATAL)
75
+ -f, --format FORMAT Output format: json, markdown, html (default: json)
76
+ html option generates a standalone web page with charts
77
+ -d, --destination DEST Output destination: stdout or log (default: stdout)
78
+ -c, --[no-]concise Use concise output format (overview + combined only, default: true)
79
+ -q, --[no-]graphql Use GraphQL API for better performance and data quality (default: true)
80
+ Use --no-graphql to fall back to REST API
81
+
82
+ HTML Output Options:
83
+ --html-theme THEME Theme for HTML output: default, dark, light (default: default)
84
+ --html-title TITLE Custom title for HTML output
85
+ --[no-]html-charts Include interactive charts in HTML output (default: true)
86
+
87
+ Examples:
88
+ github-daily-digest --token YOUR_TOKEN --gemini-key YOUR_KEY --org acme-inc
89
+ github-daily-digest --window 2.days --verbose
90
+ github-daily-digest --format markdown --destination log
91
+ github-daily-digest --org "org1,org2,org3" --format markdown
92
+
93
+ Environment variables can also be used instead of command-line options:
94
+ GITHUB_TOKEN, GEMINI_API_KEY, GITHUB_ORG_NAME, GEMINI_MODEL, FETCH_WINDOW, LOG_LEVEL,
95
+ OUTPUT_FORMAT, OUTPUT_DESTINATION, CONCISE_OUTPUT, USE_GRAPHQL
96
+ HELP
97
+ end
98
+
99
+ # Check if we need to show help
100
+ def help_requested?
101
+ @help_requested
102
+ end
103
+
104
+ # Returns number of days in the configured time window
105
+ def time_window_days
106
+ if @fetch_window_duration
107
+ # Handle ActiveSupport::Duration objects
108
+ if @fetch_window_duration.respond_to?(:in_days)
109
+ return @fetch_window_duration.in_days.to_i
110
+ # Handle string durations like '7.days'
111
+ elsif @fetch_window_duration.is_a?(String)
112
+ match = @fetch_window_duration.match(/(\d+)\.days/)
113
+ return match ? match[1].to_i : 7
114
+ end
115
+ end
116
+
117
+ # Default to 7 days if fetch_window_duration not set or has unexpected type
118
+ return 7
119
+ end
120
+
121
+ private
122
+
123
+ def parse_command_line_args(args)
124
+ parser = OptionParser.new do |opts|
125
+ opts.banner = "Usage: github-daily-digest [options]"
126
+
127
+ opts.on("-h", "--help", "Show help") do
128
+ puts help_text
129
+ @help_requested = true
130
+ end
131
+
132
+ opts.on("-t", "--token TOKEN", "GitHub API token") do |token|
133
+ ENV['GITHUB_TOKEN'] = token
134
+ end
135
+
136
+ opts.on("-g", "--gemini-key KEY", "Gemini API key") do |key|
137
+ ENV['GEMINI_API_KEY'] = key
138
+ end
139
+
140
+ opts.on("-o", "--org NAME", "GitHub organization name(s)") do |org|
141
+ ENV['GITHUB_ORG_NAME'] = org
142
+ end
143
+
144
+ opts.on("-u", "--users USERNAMES", "Specific users to process (comma-separated)") do |users|
145
+ @specific_users = users.split(',').map(&:strip).reject(&:empty?)
146
+ end
147
+
148
+ opts.on("-m", "--model MODEL", "Gemini model to use") do |model|
149
+ @gemini_model = model
150
+ end
151
+
152
+ opts.on("-w", "--window DURATION", "Time window for fetching data (e.g., '1.day')") do |window|
153
+ ENV['FETCH_WINDOW'] = window
154
+ end
155
+
156
+ opts.on("-v", "--verbose", "Enable verbose output (instead of JSON-only)") do
157
+ @json_only = false
158
+ end
159
+
160
+ opts.on("-l", "--log-level LEVEL", "Set log level (DEBUG, INFO, WARN, ERROR, FATAL)") do |level|
161
+ ENV['LOG_LEVEL'] = level
162
+ end
163
+
164
+ opts.on("-f", "--format FORMAT", "Output format (json, markdown, html)") do |format|
165
+ @output_formats = format.downcase.split(',').map(&:strip).map(&:downcase)
166
+ unless @output_formats.all? { |f| ['json', 'markdown', 'html'].include?(f) }
167
+ puts "Error: Invalid output format(s) '#{format}'. Must be one or more of 'json', 'markdown', or 'html'."
168
+ exit(1)
169
+ end
170
+ end
171
+
172
+ opts.on("-d", "--destination DEST", "Output destination (stdout, log)") do |dest|
173
+ @output_destination = dest.downcase
174
+ unless ['stdout', 'log'].include?(@output_destination)
175
+ puts "Error: Invalid output destination '#{dest}'. Must be 'stdout' or 'log'."
176
+ exit(1)
177
+ end
178
+ @output_to_stdout = (@output_destination == 'stdout')
179
+ end
180
+
181
+ opts.on("-c", "--[no-]concise", "Use concise output format (overview + combined only)") do |concise|
182
+ @concise_output = concise
183
+ end
184
+
185
+ opts.on("-q", "--[no-]graphql", "Use GraphQL API for better performance and data quality") do |graphql|
186
+ @use_graphql = graphql
187
+ @no_graphql = !graphql
188
+ end
189
+
190
+ # HTML-specific options
191
+ opts.on("--html-theme THEME", "Theme for HTML output (default, dark, light)") do |theme|
192
+ unless ['default', 'dark', 'light'].include?(theme.downcase)
193
+ puts "Error: Invalid HTML theme '#{theme}'. Must be 'default', 'dark', or 'light'."
194
+ exit(1)
195
+ end
196
+ @html_theme = theme.downcase
197
+ end
198
+
199
+ opts.on("--html-title TITLE", "Custom title for HTML output") do |title|
200
+ @html_title = title
201
+ end
202
+
203
+ opts.on("--[no-]html-charts", "Include interactive charts in HTML output") do |show_charts|
204
+ @html_show_charts = show_charts
205
+ end
206
+ end
207
+
208
+ begin
209
+ parser.parse!(args)
210
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
211
+ puts "Error: #{e.message}"
212
+ puts parser
213
+ exit(1)
214
+ end
215
+ end
216
+
217
+ def load_env_vars
218
+ @github_token = ENV['GITHUB_TOKEN']
219
+ @gemini_api_key = ENV['GEMINI_API_KEY']
220
+ @github_org_name = ENV['GITHUB_ORG_NAME']
221
+ # Parse multiple organizations if provided (comma-separated)
222
+ @github_org_names = @github_org_name.split(',').map(&:strip) if @github_org_name
223
+ @log_level = ENV.fetch('LOG_LEVEL', 'INFO').upcase
224
+ @fetch_window_duration_str = ENV.fetch('FETCH_WINDOW', '7.days')
225
+ @max_api_retries = ENV.fetch('MAX_API_RETRIES', '3').to_i
226
+ @rate_limit_sleep_base = ENV.fetch('RATE_LIMIT_SLEEP_BASE', '5').to_i
227
+ end
228
+
229
+ def validate_config
230
+ return if @help_requested
231
+
232
+ raise ArgumentError, "Missing required env var or option: GITHUB_TOKEN" unless @github_token
233
+ raise ArgumentError, "Missing required env var or option: GEMINI_API_KEY" unless @gemini_api_key
234
+ raise ArgumentError, "Missing required env var or option: GITHUB_ORG_NAME" unless @github_org_name
235
+ validate_log_level
236
+ parse_fetch_window
237
+ end
238
+
239
+ def validate_log_level
240
+ valid_levels = %w[DEBUG INFO WARN ERROR FATAL]
241
+ unless valid_levels.include?(@log_level)
242
+ raise ArgumentError, "Invalid LOG_LEVEL: #{@log_level}. Must be one of #{valid_levels.join(', ')}"
243
+ end
244
+ end
245
+
246
+ def parse_fetch_window
247
+ # Attempt to parse the duration string (e.g., "1.day", "24.hours")
248
+ @fetch_window_duration = eval(@fetch_window_duration_str) # Use eval carefully here
249
+ unless @fetch_window_duration.is_a?(ActiveSupport::Duration)
250
+ raise ArgumentError, "Invalid FETCH_WINDOW format: '#{@fetch_window_duration_str}'. Use ActiveSupport format (e.g., '1.day', '24.hours')."
251
+ end
252
+ rescue SyntaxError, NameError => e
253
+ raise ArgumentError, "Error parsing FETCH_WINDOW: '#{@fetch_window_duration_str}'. #{e.message}"
254
+ end
255
+
256
+ def calculate_time_window
257
+ @time_since = (@fetch_window_duration.ago).iso8601
258
+ end
259
+ end
260
+ end