Skip to content

Instantly share code, notes, and snippets.

@yanshiyason
Created April 3, 2025 04:35
Show Gist options
  • Save yanshiyason/45326a617622c2b5bb3ce1bc5ed954e3 to your computer and use it in GitHub Desktop.
Save yanshiyason/45326a617622c2b5bb3ce1bc5ed954e3 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require 'yaml'
require 'json'
require 'pathname'
require 'open3'
require 'set'
require 'ripper' # For Ruby comment stripping
# Load configuration from .ziprc.local or .ziprc
config_path = Pathname.new(Dir.pwd).join('.ziprc.local')
config_path = Pathname.new(Dir.pwd).join('.ziprc') unless config_path.exist?
if File.exist?(config_path)
config = YAML.load_file(config_path)
EXCLUDED_FILES = config['excluded_files'] || []
EXCLUDED_DIRECTORIES = config['excluded_directories'] || []
EXCLUDED_PATTERNS = (config['excluded_patterns'] || []).map { |pattern| Regexp.new(pattern) }
RESPECT_GITIGNORE = config.dig('settings', 'respect_gitignore') == true
STRIP_RUBY_COMMENTS = config.dig('settings', 'strip_ruby_comments') == true
else
puts "Warning: .ziprc file not found. Using default exclusions."
EXCLUDED_FILES = []
EXCLUDED_DIRECTORIES = []
EXCLUDED_PATTERNS = []
RESPECT_GITIGNORE = true
STRIP_RUBY_COMMENTS = true
end
# Utility method to convert bytes to human readable format
def human_readable_size(size)
units = ['B', 'KB', 'MB', 'GB', 'TB']
index = 0
while size >= 1024 && index < units.length - 1
size /= 1024.0
index += 1
end
"%.2f %s" % [size, units[index]]
end
# Ruby comment stripper using Ripper
def strip_ruby_comments(content)
Ripper.lex(content)
.reject { |(_, type, _)| type == :on_comment }
.map { |(_, _, str)| str }
.join
rescue => e
# If there's any error, return the original content
puts "Warning: Error stripping comments from Ruby file: #{e.message}"
return content
end
# Track all files added to the zip and their sizes
TRACKED_SIZE_DATA = []
# Get a set of all git tracked files
def get_git_tracked_files
return Set.new unless system('git rev-parse --is-inside-work-tree >/dev/null 2>&1')
stdout, status = Open3.capture2('git', 'ls-files')
if status.success?
Set.new(stdout.split("\n").map { |f| File.expand_path(f) })
else
Set.new
end
end
# Initialize set of tracked files if git ignore is respected
TRACKED_FILES = RESPECT_GITIGNORE ? get_git_tracked_files : nil
# Get the current directory name
folder_name = File.basename(Dir.pwd)
zip_filename = "tmp/#{folder_name}.zip.txt"
# Create tmp directory if it doesn't exist
Dir.mkdir('tmp') unless Dir.exist?('tmp')
# Method to check if a file should be excluded
def should_exclude?(path, base_dir)
# Skip if not tracked by git and we respect gitignore
if RESPECT_GITIGNORE && !TRACKED_FILES.include?(File.expand_path(path)) && !File.directory?(path)
return [true, "not in git"]
end
relative_path = Pathname.new(path).relative_path_from(Pathname.new(base_dir)).to_s
# Exclude whole directories
if EXCLUDED_DIRECTORIES.any? { |dir| relative_path.split('/').include?(dir) }
return [true, "excluded directory"]
end
# Exclude specific files
if EXCLUDED_FILES.include?(File.basename(relative_path))
return [true, "excluded file"]
end
# Exclude by file pattern
EXCLUDED_PATTERNS.each do |pattern|
if relative_path.match?(pattern)
return [true, "excluded pattern"]
end
end
[false, ""]
end
# Method to recursively add files to the zip archive
def add_files_to_zip(zipfile, base_dir, current_dir)
Dir.foreach(current_dir) do |file|
next if file == '.' || file == '..' # Skip system entries
path = File.join(current_dir, file)
# Skip excluded files and directories
excluded, reason = should_exclude?(path, base_dir)
if excluded
# puts "Skipping: #{path.sub("#{base_dir}/", '')} (#{reason})"
next
end
if File.directory?(path)
add_files_to_zip(zipfile, base_dir, path) # Recurse into directories
else
relative_path = Pathname.new(path).relative_path_from(Pathname.new(base_dir)).to_s
original_size = File.size(path)
# puts "Adding: #{relative_path}"
content = File.read(path)
# Strip Ruby comments if enabled and file is a Ruby file
if STRIP_RUBY_COMMENTS && File.extname(relative_path) == '.rb'
content = strip_ruby_comments(content)
end
zipfile << "<file filepath=#{relative_path}>\n#{content}\n</file>\n"
# Track file size data
stripped_size = content.bytesize
TRACKED_SIZE_DATA << {
path: relative_path,
size: stripped_size,
original_size: original_size,
size_reduction: original_size - stripped_size
}
end
end
end
# Create the ZIP file
File.open(zip_filename, 'w') do |zipfile|
# zipfile << '<project_trees>\n'
# zipfile << `rg --files | tree --fromfile`
# zipfile << '</project_trees>\n'
zipfile << "<project_files>\n"
add_files_to_zip(zipfile, Dir.pwd, Dir.pwd)
zipfile << "</project_files>\n"
end
puts "\n✅ ZIP TXT file created: #{zip_filename}"
# Separate Ruby files (which had comments stripped) from other files
ruby_files = TRACKED_SIZE_DATA.select { |item| File.extname(item[:path]) == '.rb' }
non_ruby_files = TRACKED_SIZE_DATA.select { |item| File.extname(item[:path]) != '.rb' }
# Print a simple list of all files included in the zip
puts "\n📄 Files included in the ZIP (#{TRACKED_SIZE_DATA.count} total):"
puts "=" * 85
puts "%-70s %15s" % ["File Path", "Size"]
puts "-" * 85
# Sort files by size in descending order
TRACKED_SIZE_DATA.sort_by { |item| -item[:size] }.each do |file_data|
puts "%-70s %15s" % [
file_data[:path],
human_readable_size(file_data[:size])
]
end
# Show summary statistics
puts "=" * 85
puts "%-70s %15s" % ["RUBY FILES", "#{ruby_files.count} files"]
puts "%-70s %15s" % ["OTHER FILES", "#{non_ruby_files.count} files"]
puts "%-70s %15s" % ["TOTAL FILES", "#{TRACKED_SIZE_DATA.count} files"]
puts "%-70s %15s" % ["TOTAL SIZE", human_readable_size(TRACKED_SIZE_DATA.sum { |file_data| file_data[:size] })]
# Get token count
claude_token_count = IO.popen('claude_token_count --stdin', 'r+') do |pipe|
pipe.write(File.read(zip_filename))
pipe.close_write
JSON.parse(pipe.read)['input_tokens']
end
puts "Estimated Tokens: #{claude_token_count}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment