cafeznik 0.5.5
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 +7 -0
- data/README.md +6 -0
- data/bin/cafeznik +5 -0
- data/lib/cafeznik/cli.rb +49 -0
- data/lib/cafeznik/content.rb +73 -0
- data/lib/cafeznik/log.rb +105 -0
- data/lib/cafeznik/selector.rb +78 -0
- data/lib/cafeznik/sources/base.rb +33 -0
- data/lib/cafeznik/sources/github.rb +86 -0
- data/lib/cafeznik/sources/local.rb +76 -0
- data/lib/cafeznik/sources.rb +3 -0
- data/lib/cafeznik/tool_checker.rb +16 -0
- data/lib/cafeznik/version.rb +3 -0
- data/lib/cafeznik.rb +7 -0
- metadata +267 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2d8f332ebaae2c888624a9d63af1df086999f2eadd7bceb11153e31dd5a5ac0c
|
4
|
+
data.tar.gz: 872a3e16fe91edf8062bd755e1a0746b7d4663dfd6f77e7e6e35a7f7631b25b4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cfaeda87b0b4aed7de8a7ea77314e455b82df913675807978bad611c0139b712246038686953bc1f38d3a04d41b5bd12f62979926c1445f46a33fa2c842ff3cb
|
7
|
+
data.tar.gz: 4b7fa4394308fe7d61fd05392c73c1683db062f9a103f9150e725dfd198a012a4a4ee558849113dd8889d4bfb7cb79c237ae29e232c19d78099754c3b71f6b08
|
data/README.md
ADDED
data/bin/cafeznik
ADDED
data/lib/cafeznik/cli.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
module Cafeznik
|
4
|
+
class CLI < Thor
|
5
|
+
def self.exit_on_failure? = true
|
6
|
+
def self.user_agrees? = $stdin.gets.strip.casecmp("y").zero?
|
7
|
+
|
8
|
+
class_option :verbose, type: :boolean, aliases: "-v", default: false, desc: "Run in verbose mode"
|
9
|
+
class_option :no_header, type: :boolean, default: false, desc: "Exclude headers"
|
10
|
+
class_option :with_tree, type: :boolean, aliases: "-t", default: false, desc: "Include file tree"
|
11
|
+
class_option :grep, type: :string, aliases: "-g", desc: "Filter files containing the specified content"
|
12
|
+
class_option :exclude, type: :array, aliases: "-e", desc: "Exclude files/folders matching patterns"
|
13
|
+
|
14
|
+
desc "default", "Select files, copy to clipboard; use --repo/-r for GitHub repository"
|
15
|
+
method_option :repo, type: :string, aliases: "-r", desc: "GitHub repository (owner/repo format)"
|
16
|
+
|
17
|
+
default_task :default
|
18
|
+
|
19
|
+
def default
|
20
|
+
Log.verbose = options[:verbose]
|
21
|
+
Log.info "Running in #{repo ? 'GitHub' : 'local'} mode"
|
22
|
+
|
23
|
+
source = determine_source
|
24
|
+
|
25
|
+
selector = Selector.new(source)
|
26
|
+
file_paths = selector.select
|
27
|
+
|
28
|
+
Content.new( # TODO: find better name than Content, perhaps Clipboard?
|
29
|
+
source:, file_paths:,
|
30
|
+
include_headers: !options[:no_header],
|
31
|
+
include_tree: options[:with_tree]
|
32
|
+
).copy_to_clipboard
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def determine_source
|
38
|
+
if repo
|
39
|
+
Source::GitHub.new(repo:, grep:, exclude:)
|
40
|
+
else
|
41
|
+
Source::Local.new(grep:, exclude:)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def repo = options[:repo]
|
46
|
+
def grep = options[:grep]
|
47
|
+
def exclude = options[:exclude] || []
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "clipboard"
|
2
|
+
require "memery"
|
3
|
+
|
4
|
+
module Cafeznik
|
5
|
+
class Content
|
6
|
+
include Memery
|
7
|
+
MAX_LINES = 10_000
|
8
|
+
|
9
|
+
def initialize(source:, file_paths:, include_headers:, include_tree:)
|
10
|
+
Log.debug "Initializing Content" do
|
11
|
+
<<~LOG
|
12
|
+
Source: #{source.class} file_paths: #{file_paths.size}
|
13
|
+
include_headers: #{include_headers} include_tree: #{include_tree}
|
14
|
+
LOG
|
15
|
+
end
|
16
|
+
@source = source
|
17
|
+
@file_paths = file_paths
|
18
|
+
@include_headers = include_headers
|
19
|
+
@include_tree = include_tree
|
20
|
+
end
|
21
|
+
|
22
|
+
def copy_to_clipboard
|
23
|
+
Log.debug "Copying content to clipboard"
|
24
|
+
@content = build_content
|
25
|
+
|
26
|
+
return Log.info("Copy operation cancelled by user") unless confirm_size!
|
27
|
+
|
28
|
+
::Clipboard.copy(@content)
|
29
|
+
|
30
|
+
skipped_files = @file_paths.size - files_contents.size
|
31
|
+
|
32
|
+
log_message = "Copied #{@content.lines.size} lines across #{files_contents.size} files"
|
33
|
+
log_message << " (skipped #{skipped_files} empty)" if skipped_files.positive?
|
34
|
+
|
35
|
+
Log.info("#{log_message} to clipboard")
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
memoize def build_content = [tree_section, files_contents.join("\n\n")].flatten.compact.join("\n\n")
|
41
|
+
|
42
|
+
memoize def files_contents
|
43
|
+
Log.debug "Processing #{@file_paths.size} files"
|
44
|
+
@file_paths.each_with_object([]) do |file, memo|
|
45
|
+
content = @source.content(file)
|
46
|
+
memo << (@include_headers ? with_header(content, file) : content) unless content.empty?
|
47
|
+
rescue StandardError => e
|
48
|
+
Log.error("Error fetching content for #{file}: #{e.message}")
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
memoize def tree_section = @include_tree ? with_header(@source.tree.drop(1).join("\n"), "Tree") : nil
|
54
|
+
def with_header(content, title) = "==> #{title} <==\n#{content}"
|
55
|
+
|
56
|
+
def confirm_size!
|
57
|
+
line_count = @content.lines.size
|
58
|
+
return true if line_count <= MAX_LINES
|
59
|
+
|
60
|
+
if @include_tree && suggest_tree_removal?
|
61
|
+
Log.warn "Content exceeds #{MAX_LINES} lines (#{line_count}). Try cutting out the tree? (y/N)"
|
62
|
+
@include_tree = false
|
63
|
+
@content = build_content
|
64
|
+
return confirm_size! if CLI.user_agrees?
|
65
|
+
end
|
66
|
+
|
67
|
+
Log.warn "Content exceeds #{MAX_LINES} lines (#{line_count}). Proceed? (y/N)"
|
68
|
+
CLI.user_agrees?
|
69
|
+
end
|
70
|
+
|
71
|
+
def suggest_tree_removal? = @content.lines.size <= MAX_LINES + @source.tree.size - 1
|
72
|
+
end
|
73
|
+
end
|
data/lib/cafeznik/log.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "digest"
|
3
|
+
|
4
|
+
module Cafeznik
|
5
|
+
module Log
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def verbose=(value)
|
9
|
+
@verbose = value
|
10
|
+
logger.level = value ? Logger::DEBUG : Logger::INFO
|
11
|
+
logger.formatter.verbose = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def verbose? = @verbose || false
|
15
|
+
|
16
|
+
def logger
|
17
|
+
@_logger ||= Logger.new($stdout).tap do |log|
|
18
|
+
log.level = verbose? ? Logger::DEBUG : Logger::INFO
|
19
|
+
log.formatter = CompactFormatter.new
|
20
|
+
log.debug "Verbose mode enabled" if verbose?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
%i[info debug warn error fatal].each do |level|
|
25
|
+
define_method(level) do |msg = nil, &block|
|
26
|
+
return unless logger.send(:"#{level}?")
|
27
|
+
|
28
|
+
caller_context = caller_locations(1, 1).first
|
29
|
+
component = caller_context.path[%r{/([^/]+)\.rb$}, 1]&.capitalize || "Unknown"
|
30
|
+
method = caller_context.label.split(/[#.]/).last
|
31
|
+
|
32
|
+
source_prefix = "[#{component}::#{method}]"
|
33
|
+
|
34
|
+
message = block ? "#{msg}:\n#{block.call}" : msg
|
35
|
+
formatted_message = "#{source_prefix} #{message}"
|
36
|
+
|
37
|
+
logger.send(level, formatted_message)
|
38
|
+
return unless level == :fatal
|
39
|
+
|
40
|
+
exit(1)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class CompactFormatter < Logger::Formatter
|
46
|
+
COLOR_MAP = {
|
47
|
+
colors: (30..37).to_a + (90..97).to_a # ANSI text colors
|
48
|
+
}.freeze
|
49
|
+
|
50
|
+
COLORS = {
|
51
|
+
severity: {
|
52
|
+
"DEBUG" => ["\e[44m", "\e[37m"], # Blue bg, white fg
|
53
|
+
"INFO" => ["\e[42m", "\e[30m"], # Green bg, black fg
|
54
|
+
"WARN" => ["\e[43m", "\e[30m"], # Yellow bg, black fg
|
55
|
+
"ERROR" => ["\e[41m", "\e[37m"], # Red bg, white fg
|
56
|
+
"FATAL" => ["\e[45m", "\e[30m"] # Magenta bg, black fg
|
57
|
+
},
|
58
|
+
reset: "\e[0m"
|
59
|
+
}.freeze
|
60
|
+
|
61
|
+
attr_accessor :verbose
|
62
|
+
|
63
|
+
def initialize
|
64
|
+
@component_colors = {}
|
65
|
+
@verbose = false
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
def call(severity, _time, _progname, message)
|
70
|
+
component, method, content = parse_message(message)
|
71
|
+
severity_bg, severity_fg = COLORS[:severity][severity] || COLORS[:severity]["DEBUG"]
|
72
|
+
severity_prefix = "#{severity_bg}#{severity_fg}#{severity[0]}#{COLORS[:reset]}"
|
73
|
+
source_prefix = format_source(component, method)
|
74
|
+
|
75
|
+
formatted_content = content.gsub("\n", "\n" + (" " * (severity_prefix.size + source_prefix.size + 1)))
|
76
|
+
if @verbose
|
77
|
+
"#{severity_prefix} #{source_prefix} #{formatted_content}\n"
|
78
|
+
else
|
79
|
+
"#{formatted_content}\n"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def parse_message(message)
|
86
|
+
if message =~ /\[([^:]+)::([^\]]+)\]\s+(.+)/m
|
87
|
+
[::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3)]
|
88
|
+
else
|
89
|
+
["Unknown", "unknown", message]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def format_source(component, method)
|
94
|
+
component_color = color_for_string(component)
|
95
|
+
method_color = color_for_string(method)
|
96
|
+
|
97
|
+
"[#{component_color}#{component}#{COLORS[:reset]}::#{method_color}#{method}#{COLORS[:reset]}]"
|
98
|
+
end
|
99
|
+
|
100
|
+
def color_for_string(str)
|
101
|
+
index = Digest::MD5.hexdigest(str).to_i(16) % COLOR_MAP[:colors].size
|
102
|
+
"\e[#{COLOR_MAP[:colors][index]}m"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "tty-command"
|
2
|
+
|
3
|
+
module Cafeznik
|
4
|
+
class Selector
|
5
|
+
MAX_FILES = 20
|
6
|
+
|
7
|
+
def initialize(source)
|
8
|
+
Log.fatal "fzf is kinda the centerpiece of this little tool here. Go install, deal. I'll be here when you're done" unless ToolChecker.fzf_available?
|
9
|
+
@source = source
|
10
|
+
end
|
11
|
+
|
12
|
+
def select
|
13
|
+
skip_selection if @source.tree.empty?
|
14
|
+
select_paths_with_fzf
|
15
|
+
.tap { log_selection(it) if Log.verbose? }
|
16
|
+
.then { |paths| expand_paths(paths) }
|
17
|
+
.tap { |expanded| confirm_count!(expanded) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def skip_selection
|
23
|
+
Log.info("No matching files found; skipping file selection.")
|
24
|
+
exit(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
def select_paths_with_fzf
|
28
|
+
Log.debug "Running fzf"
|
29
|
+
run_fzf_command.then { |selected| selected.include?("./") ? [:all_files] : selected }
|
30
|
+
rescue TTY::Command::ExitError => e
|
31
|
+
handle_fzf_error(e)
|
32
|
+
end
|
33
|
+
|
34
|
+
def log_selection(paths)
|
35
|
+
Log.debug("#{paths.size} paths selected:") do
|
36
|
+
paths.map.with_index(1) { |p, i| "#{i}. #{p}" }.join("\n")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def run_fzf_command = TTY::Command.new(printer: Log.verbose? ? :pretty : :null)
|
41
|
+
.run("fzf --multi", stdin: @source.tree.join("\n"))
|
42
|
+
.out.split("\n")
|
43
|
+
|
44
|
+
def handle_fzf_error(error)
|
45
|
+
exit_code = error.message.match(/exit status: (\d+)/)[1].to_i
|
46
|
+
if exit_code == 130
|
47
|
+
Log.info("No files selected. Exiting..")
|
48
|
+
exit(0)
|
49
|
+
else
|
50
|
+
Log.error("Error running fzf: #{error.message}")
|
51
|
+
exit(1)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def expand_paths(paths)
|
56
|
+
# TODO: I think I can remove the reject here, as it's already done in the source
|
57
|
+
return @source.all_files.reject { |path| @source.exclude?(path) } if paths == [:all_files]
|
58
|
+
|
59
|
+
paths.flat_map do |path|
|
60
|
+
dir?(path) ? @source.expand_dir(path) : path
|
61
|
+
end.uniq
|
62
|
+
end
|
63
|
+
|
64
|
+
def confirm_count!(paths)
|
65
|
+
Log.info "Selected #{paths.size} files"
|
66
|
+
return paths if paths.size <= MAX_FILES
|
67
|
+
|
68
|
+
Log.warn "Selected more than #{MAX_FILES} files. Continue? (y/N)"
|
69
|
+
unless CLI.user_agrees?
|
70
|
+
Log.info "Copy operation cancelled by user"
|
71
|
+
exit 0
|
72
|
+
end
|
73
|
+
paths
|
74
|
+
end
|
75
|
+
|
76
|
+
def dir?(path) = @source.dir?(path)
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Cafeznik
|
2
|
+
module Source
|
3
|
+
class Base
|
4
|
+
# TODO: change to `root: nil, repo: nil`
|
5
|
+
def initialize(repo: nil, grep: nil, exclude: [])
|
6
|
+
@repo = repo
|
7
|
+
@grep = grep
|
8
|
+
@exclude = exclude
|
9
|
+
end
|
10
|
+
|
11
|
+
def tree = raise NotImplementedError
|
12
|
+
def expand_dir(_) = raise NotImplementedError
|
13
|
+
def content(_) = raise NotImplementedError
|
14
|
+
def dir?(_) = raise NotImplementedError
|
15
|
+
def full_tree = raise NotImplementedError
|
16
|
+
|
17
|
+
def exclude?(path)
|
18
|
+
Log.debug "Checking exclusion for #{path} against #{@exclude}"
|
19
|
+
excluded = @exclude.any? do |pattern|
|
20
|
+
if pattern.include?(File::SEPARATOR) || pattern.include?("/")
|
21
|
+
File.fnmatch?(pattern, path, File::FNM_PATHNAME)
|
22
|
+
else
|
23
|
+
File.fnmatch?(pattern, File.basename(path))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
Log.debug "Exclusion result: #{excluded}"
|
27
|
+
excluded
|
28
|
+
end
|
29
|
+
|
30
|
+
def all_files = tree.reject(&method(:dir?))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
require "octokit"
|
3
|
+
require "base64"
|
4
|
+
require "resolv" # TODO: why is this here?
|
5
|
+
|
6
|
+
module Cafeznik
|
7
|
+
module Source
|
8
|
+
class GitHub < Base
|
9
|
+
def initialize(repo:, grep: nil, exclude: [])
|
10
|
+
super
|
11
|
+
@client = Octokit::Client.new(access_token:, auto_paginate: true)
|
12
|
+
verify_connection!
|
13
|
+
normalize_repo_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def tree
|
17
|
+
@_tree ||= begin
|
18
|
+
all_paths = @grep ? grep_files(@grep) : full_tree
|
19
|
+
all_paths.reject { |path| exclude?(path) }
|
20
|
+
end
|
21
|
+
rescue Octokit::Error => e
|
22
|
+
Log.error "Error fetching GitHub tree: #{e.message}"
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def content(path)
|
27
|
+
Base64.decode64 @client.contents(@repo, path:)[:content]
|
28
|
+
rescue Octokit::Error => e
|
29
|
+
Log.error "Error fetching GitHub content: #{e.message}"
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def expand_dir(path) = tree.select { _1.start_with?(path) && !_1.end_with?("/") }
|
34
|
+
def dir?(path) = path.end_with?("/")
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def verify_connection!
|
39
|
+
@client.repository(@repo)
|
40
|
+
rescue Octokit::Error, Faraday::Error => e
|
41
|
+
error_messages = {
|
42
|
+
Faraday::ConnectionFailed => "You might be offline, or something is keeping you from connecting 🛜",
|
43
|
+
Octokit::Unauthorized => "Unable to connect to GitHub. Please check your token / gh cli 🐙",
|
44
|
+
Octokit::NotFound => "Repo not found. Can't help you 🪬"
|
45
|
+
}
|
46
|
+
Log.fatal error_messages[e.class] || e.message
|
47
|
+
end
|
48
|
+
|
49
|
+
def normalize_repo_name
|
50
|
+
@repo = @repo[%r{github\.com[:/](.+?)(/?$)}, 1] || @repo.delete_prefix("/").delete_suffix("/")
|
51
|
+
end
|
52
|
+
|
53
|
+
def access_token = @_access_token ||=
|
54
|
+
ENV["GITHUB_TOKEN"] ||
|
55
|
+
fetch_token_via_gh ||
|
56
|
+
(Log.error("GitHub token not found. Please configure `gh` or set GITHUB_TOKEN in your environment.")
|
57
|
+
exit 1)
|
58
|
+
|
59
|
+
def full_tree
|
60
|
+
branch = @client.repository(@repo).default_branch
|
61
|
+
# get all all paths and add a trailing slash for directories
|
62
|
+
paths = @client.tree(@repo, branch, recursive: true).tree.map { "#{_1.path}#{'/' if _1.type == 'tree'}" }
|
63
|
+
(["./"] + paths).sort
|
64
|
+
end
|
65
|
+
|
66
|
+
def fetch_token_via_gh
|
67
|
+
Log.debug("Fetching GitHub token via GitHub CLI")
|
68
|
+
Log.fatal "GitHub CLI not installed. Either install it or set GITHUB_TOKEN in your environment" unless ToolChecker.gh_available?
|
69
|
+
TTY::Command.new(printer: :null).run("gh auth token").out.strip
|
70
|
+
rescue TTY::Command::ExitError
|
71
|
+
Log.warn("Failed to fetch GitHub token via GitHub CLI. Install GH and authenticate with `gh auth login`, or set GITHUB_TOKEN in your environment")
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def grep_files(pattern)
|
76
|
+
Log.debug "Searching for pattern '#{pattern}' within #{@repo}"
|
77
|
+
results = @client.search_code("#{pattern} repo:#{@repo} in:file").items.map(&:path)
|
78
|
+
Log.debug "Found #{results.size} files matching pattern '#{pattern}' in #{@repo}"
|
79
|
+
results
|
80
|
+
rescue Octokit::Error => e
|
81
|
+
Log.error "Error during search for pattern '#{pattern}': #{e.message}"
|
82
|
+
[]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
require "tty-command"
|
3
|
+
require "memery"
|
4
|
+
|
5
|
+
module Cafeznik
|
6
|
+
module Source
|
7
|
+
class Local < Base
|
8
|
+
include Memery
|
9
|
+
def initialize(grep: nil, exclude: [])
|
10
|
+
super
|
11
|
+
Log.fatal "fd not installed. We depend on it. Get it!" unless ToolChecker.fd_available?
|
12
|
+
|
13
|
+
@cmd = TTY::Command.new(printer: Log.verbose? ? :pretty : :null)
|
14
|
+
end
|
15
|
+
|
16
|
+
memoize def tree
|
17
|
+
Log.debug "Building file tree#{@grep ? ' with grep filter' : ''}, #{@exclude ? "excluding: #{@exclude.join(',')}" : ''}"
|
18
|
+
files = @grep ? grepped_files : full_tree
|
19
|
+
files.empty? ? [] : ["./"] + files.sort
|
20
|
+
end
|
21
|
+
|
22
|
+
def expand_dir(path)
|
23
|
+
if path == "./"
|
24
|
+
return grepped_files if @grep
|
25
|
+
|
26
|
+
return full_tree
|
27
|
+
end
|
28
|
+
|
29
|
+
list_paths(path, files_only: true)
|
30
|
+
end
|
31
|
+
|
32
|
+
def dir?(path) = File.directory?(path)
|
33
|
+
|
34
|
+
def content(path)
|
35
|
+
return nil if dir?(path) || exclude?(path)
|
36
|
+
|
37
|
+
File.read(path)
|
38
|
+
rescue StandardError
|
39
|
+
Log.error("File not found: #{path}")
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def full_tree = list_paths
|
44
|
+
def all_files = @grep ? grepped_files : list_paths(files_only: true)
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def list_paths(path = ".", files_only: false)
|
49
|
+
args = ["--hidden", "--follow",
|
50
|
+
(["--type", "f"] if files_only),
|
51
|
+
"--full-path",
|
52
|
+
*exclusion_args,
|
53
|
+
path].flatten.compact
|
54
|
+
run_cmd("fd", args)
|
55
|
+
rescue TTY::Command::ExitError => e
|
56
|
+
Log.error("FD error: #{e.message}") unless e.message.include?("exit status: 1")
|
57
|
+
[]
|
58
|
+
end
|
59
|
+
|
60
|
+
def exclusion_args = (@exclude + [".git"]).flat_map { |p| ["--exclude", p] }
|
61
|
+
|
62
|
+
memoize def grepped_files
|
63
|
+
Log.fatal "rg required for grep functionality. Install and retry." unless ToolChecker.rg_available?
|
64
|
+
|
65
|
+
args = @exclude.flat_map { |p| ["-g", "!#{p}"] } # formats the exclusion into rg glob format
|
66
|
+
result = run_cmd("rg", ["--files-with-matches", @grep, ".", *args])
|
67
|
+
result.map { |f| f.delete_prefix("./") }
|
68
|
+
rescue TTY::Command::ExitError => e
|
69
|
+
Log.error("RG error: #{e.message}") unless e.message.include?("exit status: 1")
|
70
|
+
[]
|
71
|
+
end
|
72
|
+
|
73
|
+
def run_cmd(cmd, args) = @cmd.run(cmd, *args).out.split("\n")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Cafeznik
|
2
|
+
module ToolChecker
|
3
|
+
def self.method_missing(method_name)
|
4
|
+
if method_name.to_s =~ /^(.+)_available\?$/
|
5
|
+
tool_name = Regexp.last_match(1)
|
6
|
+
system("command -v #{tool_name} > /dev/null 2>&1")
|
7
|
+
else
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.respond_to_missing?(method_name, include_private = false)
|
13
|
+
method_name.to_s.end_with?("_available?") || super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/cafeznik.rb
ADDED
metadata
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cafeznik
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lem
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-02-19 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: base64
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0.2'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0.2'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: clipboard
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: faraday-multipart
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: faraday-retry
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '2.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: memery
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.6'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.6'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: octokit
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '9.2'
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '9.2'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: thor
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '1.3'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.3'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: tty-command
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0.10'
|
117
|
+
type: :runtime
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0.10'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: rspec
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '3.13'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '3.13'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: rubocop
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '1.66'
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '1.66'
|
152
|
+
- !ruby/object:Gem::Dependency
|
153
|
+
name: rubocop-performance
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - "~>"
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '1.22'
|
159
|
+
type: :development
|
160
|
+
prerelease: false
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - "~>"
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '1.22'
|
166
|
+
- !ruby/object:Gem::Dependency
|
167
|
+
name: rubocop-rspec
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - "~>"
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '3.2'
|
173
|
+
type: :development
|
174
|
+
prerelease: false
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - "~>"
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '3.2'
|
180
|
+
- !ruby/object:Gem::Dependency
|
181
|
+
name: standard
|
182
|
+
requirement: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - "~>"
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '1.41'
|
187
|
+
type: :development
|
188
|
+
prerelease: false
|
189
|
+
version_requirements: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - "~>"
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '1.41'
|
194
|
+
- !ruby/object:Gem::Dependency
|
195
|
+
name: super_diff
|
196
|
+
requirement: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
type: :development
|
202
|
+
prerelease: false
|
203
|
+
version_requirements: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - ">="
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: '0'
|
208
|
+
- !ruby/object:Gem::Dependency
|
209
|
+
name: webmock
|
210
|
+
requirement: !ruby/object:Gem::Requirement
|
211
|
+
requirements:
|
212
|
+
- - "~>"
|
213
|
+
- !ruby/object:Gem::Version
|
214
|
+
version: '3.24'
|
215
|
+
type: :development
|
216
|
+
prerelease: false
|
217
|
+
version_requirements: !ruby/object:Gem::Requirement
|
218
|
+
requirements:
|
219
|
+
- - "~>"
|
220
|
+
- !ruby/object:Gem::Version
|
221
|
+
version: '3.24'
|
222
|
+
description: 'A CLI tool for copying files to your clipboard en masse, from a local
|
223
|
+
directory or a GitHub repository. Why? So you can feed them into LLMs like a lazy
|
224
|
+
lazy script kiddie.
|
225
|
+
|
226
|
+
'
|
227
|
+
executables:
|
228
|
+
- cafeznik
|
229
|
+
extensions: []
|
230
|
+
extra_rdoc_files: []
|
231
|
+
files:
|
232
|
+
- README.md
|
233
|
+
- bin/cafeznik
|
234
|
+
- lib/cafeznik.rb
|
235
|
+
- lib/cafeznik/cli.rb
|
236
|
+
- lib/cafeznik/content.rb
|
237
|
+
- lib/cafeznik/log.rb
|
238
|
+
- lib/cafeznik/selector.rb
|
239
|
+
- lib/cafeznik/sources.rb
|
240
|
+
- lib/cafeznik/sources/base.rb
|
241
|
+
- lib/cafeznik/sources/github.rb
|
242
|
+
- lib/cafeznik/sources/local.rb
|
243
|
+
- lib/cafeznik/tool_checker.rb
|
244
|
+
- lib/cafeznik/version.rb
|
245
|
+
homepage: https://github.com/LemuelCushing/cafeznik
|
246
|
+
licenses:
|
247
|
+
- MIT
|
248
|
+
metadata:
|
249
|
+
rubygems_mfa_required: 'true'
|
250
|
+
rdoc_options: []
|
251
|
+
require_paths:
|
252
|
+
- lib
|
253
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
254
|
+
requirements:
|
255
|
+
- - ">="
|
256
|
+
- !ruby/object:Gem::Version
|
257
|
+
version: '3.3'
|
258
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
259
|
+
requirements:
|
260
|
+
- - ">="
|
261
|
+
- !ruby/object:Gem::Version
|
262
|
+
version: '0'
|
263
|
+
requirements: []
|
264
|
+
rubygems_version: 3.6.3
|
265
|
+
specification_version: 4
|
266
|
+
summary: CLI tool for copying files to your clipboard en masse
|
267
|
+
test_files: []
|