cafeznik 0.5.61 → 0.8.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 +4 -4
- data/README.md +125 -2
- data/lib/cafeznik/cli.rb +7 -4
- data/lib/cafeznik/content.rb +74 -15
- data/lib/cafeznik/help.rb +55 -0
- data/lib/cafeznik/selector.rb +27 -7
- data/lib/cafeznik/sources/base.rb +10 -9
- data/lib/cafeznik/sources/github.rb +24 -6
- data/lib/cafeznik/version.rb +1 -1
- data/lib/cafeznik.rb +1 -0
- metadata +36 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6a5befc473305cc3827c00be0f4b2eb1541dcce37b01f599e2c917cc9fb8716
|
4
|
+
data.tar.gz: 8cee6c1e78bee69d193ceeb9d1d1b928bc3e8d0e2a724a295b4e64aae8b1e0a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: befa9841636ca27d1d05b822ca5feb19121c620195430a79cfb0b90b2a49782f940e5419291c6875da409b9e7376d778a1ed485d4f5f12135009a7e5d309c35d
|
7
|
+
data.tar.gz: 553dd4f54051c5c927f8e4b09a642fef516516ead8e275b555222ff5ea23acf3061c705418aa28b003bc9c6d1032b8e25d65934951fbee63430ca3b6fd9af266
|
data/README.md
CHANGED
@@ -1,6 +1,129 @@
|
|
1
1
|
# Cafeznik
|
2
2
|
|
3
|
-
|
3
|
+
_Not a fez-wearing cat in a track suit._
|
4
4
|
|
5
|
+
There are [many](https://github.com/Dicklesworthstone/your-source-to-prompt.html?tab=readme-ov-file) code2prompt tools around, but this one is mine 🪖. It’s built with a sprinkle of practicality, a dash of irreverence, and plenty of laziness—exactly how a good developer tool should be.
|
6
|
+
|
7
|
+
## What is this?
|
8
|
+
|
9
|
+
Cafeznik is an interactive CLI (levereging the beautiful [fzf](https://github.com/junegunn/fzf)) to ease the selection and copying of code files - local or remote (GitHub) - to your clipboard.
|
10
|
+
|
11
|
+
Why? You know why - so I can feed it into LLMs like the lazy, lazy ~~script kiddie~~ *vibe programmer* I am. It’s streamlined, efficient, and dangerously habit-forming.
|
12
|
+
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Install it directly via RubyGems (requires Ruby 3.3 and the other dependencies [listed below](#dependencies)):
|
17
|
+
|
18
|
+
```bash
|
19
|
+
gem install cafeznik
|
20
|
+
```
|
21
|
+
|
22
|
+
## Then what?
|
23
|
+
|
24
|
+
```bash
|
25
|
+
cafeznik # or cafeznik --repo owner/repo
|
26
|
+
```
|
27
|
+
use `tab` to select multiple files, `enter` to copy them to your clipboard, and `ctrl-c` to exit.
|
28
|
+
|
29
|
+
### looksee
|
30
|
+
Local mode:
|
31
|
+
[](https://asciinema.org/a/YWcuK13nRybD234R5nkW8J7Hh)
|
32
|
+
Or remote with grep and exclude:
|
33
|
+
|
34
|
+
## Dependencies
|
35
|
+
|
36
|
+
Cafeznik relies on a few external tools to work its magic:
|
37
|
+
|
38
|
+
- [`fzf`](https://github.com/junegunn/fzf) – Essential for interactive file selection (absolutely required)
|
39
|
+
- [`fd`](https://github.com/sharkdp/fd) – Powers local file discovery (required for local mode)
|
40
|
+
- [`ripgrep`](https://github.com/BurntSushi/ripgrep) – Enables efficient grep functionality (required when using `--grep`)
|
41
|
+
- [`bat`](https://github.com/sharkdp/bat) (& `tree`) – Provide pretty previews (optional but highly recommended)
|
42
|
+
- [`gh`](https://cli.github.com/) – Simplifies GitHub authentication (optional; you can alternatively set the GITHUB_TOKEN environment variable)
|
43
|
+
|
44
|
+
A homebrew line to install all the dependencies on macOS:
|
45
|
+
|
46
|
+
```bash
|
47
|
+
brew install fzf fd ripgrep bat tree gh
|
48
|
+
```
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
### Local mode
|
53
|
+
|
54
|
+
Quickly select and copy files from your current directory:
|
55
|
+
|
56
|
+
```bash
|
57
|
+
cafeznik
|
58
|
+
```
|
59
|
+
|
60
|
+
Filter your selection to include only files that contain specific text:
|
61
|
+
|
62
|
+
```bash
|
63
|
+
cafeznik --grep "def initialize"
|
64
|
+
```
|
65
|
+
|
66
|
+
Easily exclude unwanted files or directories:
|
67
|
+
|
68
|
+
```bash
|
69
|
+
cafeznik --exclude "*.log" --exclude "tmp/"
|
70
|
+
```
|
71
|
+
|
72
|
+
### GitHub mode
|
73
|
+
|
74
|
+
Fetch and copy code directly from any GitHub repository:
|
75
|
+
|
76
|
+
```bash
|
77
|
+
cafeznik --repo owner/repo
|
78
|
+
```
|
79
|
+
|
80
|
+
It also supports full URLs:
|
81
|
+
|
82
|
+
```bash
|
83
|
+
cafeznik --repo https://github.com/owner/repo
|
84
|
+
```
|
85
|
+
|
86
|
+
## Noteworthy Flags
|
87
|
+
|
88
|
+
```
|
89
|
+
--repo, -r Specify a GitHub repository to fetch files from
|
90
|
+
--grep, -g Only select files containing specific text patterns (works locally and remotely)
|
91
|
+
--exclude, -e Exclude files or directories matching provided patterns (also works locally and remotely)
|
92
|
+
--with-tree, -t Include a detailed file tree structure in your output (Guess what? Works locally and remotely)
|
93
|
+
```
|
94
|
+
|
95
|
+
## Less important flags
|
96
|
+
```
|
97
|
+
--no-header Omit file headers from the copied content for a cleaner paste
|
98
|
+
--verbose Activate detailed logging output for debugging and transparency
|
99
|
+
```
|
100
|
+
|
101
|
+
Or, you know:
|
102
|
+
|
103
|
+
```bash
|
104
|
+
cafeznik --help
|
105
|
+
```
|
106
|
+
|
107
|
+
## What's a-comin
|
108
|
+
|
109
|
+
- History of copied files - so you can easily re-copy them. Rinse, repeat.
|
110
|
+
- Optional minification of copied files
|
111
|
+
- Binary files support for multi-modal models? Might be a stretch
|
112
|
+
- Token counting. Everyone loves token counting.
|
113
|
+
|
114
|
+
## Noteworthy Competitors I did not take inspiration from
|
115
|
+
|
116
|
+
- [gitingest](https://github.com/davidesantangelo/gitingest) - Fellow Ruby that works much better on bigger repos, and packs it all nicely in a prompt file
|
117
|
+
- [onefilellm](https://github.com/jimmc414/onefilellm) - Does so much more, expect it's a completely different thing
|
118
|
+
- [your-source-to-prompt.html](https://github.com/Dicklesworthstone/your-source-to-prompt.html) - If you wanna leave your console for a browser, you'll get plenty of nice features for your code2prompt needs
|
119
|
+
|
120
|
+
## License
|
121
|
+
|
122
|
+
Cafeznik is open-source software, licensed under the MIT License.
|
123
|
+
|
124
|
+
## Contributing
|
125
|
+
|
126
|
+
Contributions of all kinds are warmly welcomed.
|
127
|
+
|
128
|
+
Enjoy your freshly copied code snacks! 🍪
|
5
129
|
|
6
|
-
There are [many](https://github.com/Dicklesworthstone/your-source-to-prompt.html?tab=readme-ov-file) a code2prompt tools about, but this one is mine 🪖
|
data/lib/cafeznik/cli.rb
CHANGED
@@ -7,8 +7,8 @@ module Cafeznik
|
|
7
7
|
class_option :verbose, type: :boolean, aliases: "--debug", default: false, desc: "Run in verbose mode"
|
8
8
|
class_option :no_header, type: :boolean, default: false, desc: "Exclude headers"
|
9
9
|
class_option :with_tree, type: :boolean, aliases: "-t", default: false, desc: "Include file tree"
|
10
|
-
class_option :grep, type: :string, aliases: "-g", desc: "Filter files containing the specified content"
|
11
|
-
class_option :exclude, type: :array, aliases: "-e", desc: "Exclude files/folders matching patterns"
|
10
|
+
class_option :grep, type: :string, aliases: "-g", desc: "Filter files containing the specified content", banner: "Patterny Pattern"
|
11
|
+
class_option :exclude, type: :array, aliases: "-e", desc: "Exclude files/folders matching patterns", banner: "glob *.ext **/dir"
|
12
12
|
|
13
13
|
map %w[-v --version] => :version
|
14
14
|
|
@@ -37,6 +37,9 @@ module Cafeznik
|
|
37
37
|
).copy_to_clipboard
|
38
38
|
end
|
39
39
|
|
40
|
+
desc "help [COMMAND]", "Display detailed help information"
|
41
|
+
def help = Help.display(self)
|
42
|
+
|
40
43
|
private
|
41
44
|
|
42
45
|
def determine_source
|
@@ -47,8 +50,8 @@ module Cafeznik
|
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
50
|
-
def repo = options[:repo]
|
51
|
-
def grep = options[:grep]
|
52
53
|
def exclude = options[:exclude] || []
|
54
|
+
def repo = options[:repo].tap { |r| Log.fatal("We can't do much without a repo when using -r/--repo") if r == "repo" }
|
55
|
+
def grep = options[:grep].tap { |g| Log.fatal("You gotta provide a search pattern when using -g/--grep") if g == "grep" }
|
53
56
|
end
|
54
57
|
end
|
data/lib/cafeznik/content.rb
CHANGED
@@ -1,22 +1,21 @@
|
|
1
1
|
require "clipboard"
|
2
|
+
require "concurrent"
|
2
3
|
require "memery"
|
4
|
+
require "tty-progressbar"
|
3
5
|
|
4
6
|
module Cafeznik
|
5
7
|
class Content
|
6
8
|
include Memery
|
7
9
|
MAX_LINES = 10_000
|
10
|
+
THREAD_COUNT = [Concurrent.processor_count, 8].min
|
11
|
+
THREAD_TIMEOUT = 20 # seconds
|
8
12
|
|
9
13
|
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
14
|
@source = source
|
17
15
|
@file_paths = file_paths
|
18
16
|
@include_headers = include_headers
|
19
17
|
@include_tree = include_tree
|
18
|
+
log_init
|
20
19
|
end
|
21
20
|
|
22
21
|
def copy_to_clipboard
|
@@ -37,21 +36,81 @@ module Cafeznik
|
|
37
36
|
|
38
37
|
private
|
39
38
|
|
39
|
+
def log_init
|
40
|
+
Log.debug "Initializing Content" do
|
41
|
+
<<~LOG
|
42
|
+
Source: #{@source.class} file_paths: #{@file_paths.size}
|
43
|
+
include_headers: #{@include_headers} include_tree: #{@include_tree}
|
44
|
+
LOG
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
40
48
|
def build_content = [tree_section, files_contents.join("\n\n")].flatten.compact.join("\n\n")
|
41
49
|
|
50
|
+
def tree_section = @include_tree ? with_header(@source.tree.drop(1).join("\n"), "Tree") : nil
|
51
|
+
def with_header(content, title) = "==> #{title} <==\n#{content}"
|
52
|
+
|
42
53
|
memoize def files_contents
|
43
|
-
Log.debug "Processing #{@file_paths.size} files"
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
54
|
+
Log.debug "Processing #{@file_paths.size} files in #{THREAD_COUNT} threads"
|
55
|
+
|
56
|
+
bars = create_progress_bars
|
57
|
+
errors = Concurrent::Hash.new
|
58
|
+
executor = Concurrent::FixedThreadPool.new(THREAD_COUNT)
|
59
|
+
|
60
|
+
tasks = create_file_tasks(executor, bars, errors)
|
61
|
+
|
62
|
+
# Waits for all concurrent tasks to complete within the timeout, then returns their results as a compact array.
|
63
|
+
results = Concurrent::Promises.zip(*tasks).value!(THREAD_TIMEOUT).compact
|
64
|
+
|
65
|
+
executor.shutdown
|
66
|
+
executor.wait_for_termination(THREAD_TIMEOUT)
|
67
|
+
|
68
|
+
report_errors(errors) if errors.any?
|
69
|
+
results
|
70
|
+
end
|
71
|
+
|
72
|
+
def create_progress_bars
|
73
|
+
progress = TTY::ProgressBar::Multi.new("Processing files [:bar] :percent")
|
74
|
+
{
|
75
|
+
started: progress.register("Starting [:bar] :current/:total", total: @file_paths.size),
|
76
|
+
finished: progress.register("Completed [:bar] :current/:total", total: @file_paths.size)
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def create_file_tasks(executor, bars, errors)
|
81
|
+
@file_paths.map do |file|
|
82
|
+
bars[:started].advance
|
83
|
+
|
84
|
+
Concurrent::Promises.future_on(executor) do
|
85
|
+
fetch_and_format_file(file, errors)
|
86
|
+
ensure
|
87
|
+
bars[:finished].advance
|
88
|
+
end
|
50
89
|
end
|
51
90
|
end
|
52
91
|
|
53
|
-
def
|
54
|
-
|
92
|
+
def fetch_and_format_file(file, errors)
|
93
|
+
content = @source.content(file)
|
94
|
+
if content && !content.empty?
|
95
|
+
@include_headers ? with_header(content, file) : content
|
96
|
+
end
|
97
|
+
rescue StandardError => e
|
98
|
+
errors[file] = e.message
|
99
|
+
Log.error("Error fetching content for #{file}: #{e.message}")
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
def report_errors(errors)
|
104
|
+
Log.warn "Completed with #{errors.size} errors:"
|
105
|
+
errors.each.with_index(1) do |(file, message), i|
|
106
|
+
Log.warn " #{i}. #{file}: #{message}"
|
107
|
+
break if i >= 5 && errors.size > 5
|
108
|
+
end
|
109
|
+
|
110
|
+
return unless errors.size > 5
|
111
|
+
|
112
|
+
Log.warn " ... and #{errors.size - 5} more errors"
|
113
|
+
end
|
55
114
|
|
56
115
|
def confirm_size!
|
57
116
|
line_count = @content.lines.size
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# rubocop:disable Metrics/MethodLength
|
2
|
+
module Cafeznik
|
3
|
+
module Help
|
4
|
+
def self.display(cli)
|
5
|
+
version = Cafeznik::VERSION
|
6
|
+
banner = <<~BANNER
|
7
|
+
╔◤ CΛFΞΖПIK v#{version} ◢═══════════╗
|
8
|
+
║─┳─ interactive ║ code2pilfer ║
|
9
|
+
╚═╂══════════════╩══════ꙮ══════╝
|
10
|
+
BANNER
|
11
|
+
cli.say banner, :green
|
12
|
+
cli.say "\n"
|
13
|
+
|
14
|
+
cli.say_status("Usage", "cafeznik [OPTIONS]", :yellow)
|
15
|
+
cli.print_wrapped(
|
16
|
+
"The default behavior (local mode) is invoked when no subcommand is given. " \
|
17
|
+
"To use GitHub mode, specify a repository with the --repo option.", indent: 2
|
18
|
+
)
|
19
|
+
cli.say "\n"
|
20
|
+
|
21
|
+
cli.say_status("Modes", "", :blue)
|
22
|
+
modes = [
|
23
|
+
["Local Mode (default)", "Copies files from your local file system."],
|
24
|
+
["GitHub Mode", "Copies files from a GitHub repository (specify with --repo owner/repo)."]
|
25
|
+
]
|
26
|
+
cli.print_table(modes, indent: 2, borders: true)
|
27
|
+
cli.say "\n"
|
28
|
+
|
29
|
+
cli.say_status("Options", "", :cyan)
|
30
|
+
options = [
|
31
|
+
["--repo, -r", "Specify a GitHub repository (owner/repo format)"],
|
32
|
+
["--grep, -g", "Filter files: include only those containing a specific pattern"],
|
33
|
+
["--exclude", "Exclude files/folders matching given glob patterns"],
|
34
|
+
["--with_tree, -t", "Include the file tree structure in the output"],
|
35
|
+
["--no_header", "Exclude file headers from the copied content"],
|
36
|
+
["--version, -v", "Display version information"],
|
37
|
+
["--verbose", "Enable verbose logging (detailed output)"],
|
38
|
+
["-h/help", "Display this help message"]
|
39
|
+
]
|
40
|
+
cli.print_table(options, indent: 2, borders: false)
|
41
|
+
cli.say "\n"
|
42
|
+
|
43
|
+
cli.say_status("Examples", "", :green)
|
44
|
+
examples = [
|
45
|
+
["cafeznik", "# Runs in local mode, letting you select local files to copy to the clipboard"],
|
46
|
+
["cafeznik --repo LemuelCushing/cafeznik", "# Runs in GitHub mode and gets the repo"],
|
47
|
+
["cafeznik --no_header --with_tree", "# Does not include headers and includes the file tree"],
|
48
|
+
["cafeznik --grep \"ChunkyBacon.new\"", "# Only includes files where new ChunkyBacon are chunked"]
|
49
|
+
]
|
50
|
+
cli.print_table(examples, indent: 2)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# rubocop:enable Metrics/MethodLength
|
data/lib/cafeznik/selector.rb
CHANGED
@@ -5,7 +5,7 @@ module Cafeznik
|
|
5
5
|
MAX_FILES = 20
|
6
6
|
|
7
7
|
def initialize(source)
|
8
|
-
Log.fatal "fzf is kinda the centerpiece of this little tool here. Go install,
|
8
|
+
Log.fatal "fzf is kinda the centerpiece of this little tool here. Go install it, dear. I'll be here when you're done" unless ToolChecker.fzf_available?
|
9
9
|
@source = source
|
10
10
|
end
|
11
11
|
|
@@ -26,7 +26,13 @@ module Cafeznik
|
|
26
26
|
|
27
27
|
def select_paths_with_fzf
|
28
28
|
Log.debug "Running fzf"
|
29
|
-
|
29
|
+
result = run_fzf_command
|
30
|
+
if result.include?("./")
|
31
|
+
@select_all = true
|
32
|
+
["./"]
|
33
|
+
else
|
34
|
+
result
|
35
|
+
end
|
30
36
|
rescue TTY::Command::ExitError => e
|
31
37
|
handle_fzf_error(e)
|
32
38
|
end
|
@@ -37,8 +43,17 @@ module Cafeznik
|
|
37
43
|
end
|
38
44
|
end
|
39
45
|
|
46
|
+
def preview_command
|
47
|
+
return "" unless @source.is_a?(Cafeznik::Source::Local)
|
48
|
+
|
49
|
+
file_preview = ToolChecker.bat_available? ? "bat --style=numbers --color=always {}" : "tail -n +1 {}"
|
50
|
+
warn = "🌳 Preview tree may be off - greps and excludes are not taken into account 🌴\n #{'=' * 100}"
|
51
|
+
|
52
|
+
"([[ -d {} ]] && (echo '#{warn}'; tree --gitignore -C {} | head -n 50) || #{file_preview})"
|
53
|
+
end
|
54
|
+
|
40
55
|
def run_fzf_command = TTY::Command.new(printer: Log.verbose? ? :pretty : :null)
|
41
|
-
.run("fzf --multi", stdin: @source.tree.join("\n"))
|
56
|
+
.run("fzf --multi --preview \"#{preview_command}\"", stdin: @source.tree.join("\n"))
|
42
57
|
.out.split("\n")
|
43
58
|
|
44
59
|
def handle_fzf_error(error)
|
@@ -53,19 +68,24 @@ module Cafeznik
|
|
53
68
|
end
|
54
69
|
|
55
70
|
def expand_paths(paths)
|
56
|
-
|
57
|
-
|
71
|
+
if @select_all
|
72
|
+
Log.debug "Root directory selected, returning all files"
|
73
|
+
return @source.all_files
|
74
|
+
end
|
58
75
|
|
59
|
-
paths.flat_map do |path|
|
76
|
+
result = paths.flat_map do |path|
|
60
77
|
dir?(path) ? @source.expand_dir(path) : path
|
61
78
|
end.uniq
|
79
|
+
|
80
|
+
Log.debug "Expanded #{paths.size} paths to #{result.size} files"
|
81
|
+
result
|
62
82
|
end
|
63
83
|
|
64
84
|
def confirm_count!(paths)
|
65
85
|
Log.info "Selected #{paths.size} files"
|
66
86
|
return paths if paths.size <= MAX_FILES
|
67
87
|
|
68
|
-
Log.warn "Selected more than #{MAX_FILES} files. Continue? (y/N)"
|
88
|
+
Log.warn "Selected more than #{MAX_FILES} files (#{paths.size}). Continue? (y/N)"
|
69
89
|
unless CLI.user_agrees?
|
70
90
|
Log.info "Copy operation cancelled by user"
|
71
91
|
exit 0
|
@@ -3,17 +3,21 @@ module Cafeznik
|
|
3
3
|
class Base
|
4
4
|
BINARY_EXCLUDES = [
|
5
5
|
# Images and media
|
6
|
-
%w[*.png *.jpg *.jpeg *.gif *.svg *.ico
|
7
|
-
|
6
|
+
%w[*.png *.jpg *.jpeg *.gif *.svg *.ico
|
7
|
+
*.pdf *.mov *.mp4 *.mp3 *.wav *.cast],
|
8
8
|
# Archives
|
9
9
|
%w[*.zip *.tar.gz *.tgz *.rar *.7z],
|
10
10
|
# Compiled code
|
11
|
-
%w[*.pyc *.pyo *.class *.jar *.dll
|
12
|
-
|
11
|
+
%w[*.pyc *.pyo *.class *.jar *.dll
|
12
|
+
*.exe *.so *.dylib *.o *.obj],
|
13
13
|
# Minified files
|
14
14
|
%w[*.min.js *.min.css],
|
15
|
+
# Lockfiles
|
16
|
+
%w[package-lock.json yarn.lock Gemfile.lock],
|
17
|
+
# Fonts
|
18
|
+
%w[*.woff *.woff2 *.ttf *.eot *.otf],
|
15
19
|
# Pesky necessities
|
16
|
-
%w[.git .DS_Store Thumbs.db]
|
20
|
+
%w[.git .DS_Store Thumbs.db .ruby-lsp]
|
17
21
|
].flatten.freeze
|
18
22
|
|
19
23
|
# TODO: change to `root: nil, repo: nil`
|
@@ -30,16 +34,13 @@ module Cafeznik
|
|
30
34
|
def full_tree = raise NotImplementedError
|
31
35
|
|
32
36
|
def exclude?(path)
|
33
|
-
|
34
|
-
excluded = @exclude.any? do |pattern|
|
37
|
+
@exclude.any? do |pattern|
|
35
38
|
if pattern.include?(File::SEPARATOR) || pattern.include?("/")
|
36
39
|
File.fnmatch?(pattern, path, File::FNM_PATHNAME)
|
37
40
|
else
|
38
41
|
File.fnmatch?(pattern, File.basename(path))
|
39
42
|
end
|
40
43
|
end
|
41
|
-
Log.debug "Exclusion result: #{excluded}"
|
42
|
-
excluded
|
43
44
|
end
|
44
45
|
|
45
46
|
def all_files = tree.reject(&method(:dir?))
|
@@ -5,6 +5,9 @@ require "base64"
|
|
5
5
|
module Cafeznik
|
6
6
|
module Source
|
7
7
|
class GitHub < Base
|
8
|
+
MAX_RETRIES = 3
|
9
|
+
BASE_DELAY = 2
|
10
|
+
|
8
11
|
def initialize(repo:, grep: nil, exclude: [])
|
9
12
|
super
|
10
13
|
@client = Octokit::Client.new(access_token:, auto_paginate: true)
|
@@ -14,8 +17,8 @@ module Cafeznik
|
|
14
17
|
|
15
18
|
def tree
|
16
19
|
@_tree ||= begin
|
17
|
-
all_paths = @grep ? grep_files(@grep) : full_tree
|
18
|
-
all_paths.reject {
|
20
|
+
all_paths = (@grep ? grep_files(@grep) : full_tree)
|
21
|
+
all_paths.reject { exclude?(it) }.push("./").sort
|
19
22
|
end
|
20
23
|
rescue Octokit::Error => e
|
21
24
|
Log.error "Error fetching GitHub tree: #{e.message}"
|
@@ -23,7 +26,7 @@ module Cafeznik
|
|
23
26
|
end
|
24
27
|
|
25
28
|
def content(path)
|
26
|
-
|
29
|
+
fetch_file_content_with_retry(path)
|
27
30
|
rescue Octokit::Error => e
|
28
31
|
Log.error "Error fetching GitHub content: #{e.message}"
|
29
32
|
nil
|
@@ -57,9 +60,7 @@ module Cafeznik
|
|
57
60
|
|
58
61
|
def full_tree
|
59
62
|
branch = @client.repository(@repo).default_branch
|
60
|
-
|
61
|
-
paths = @client.tree(@repo, branch, recursive: true).tree.map { "#{it.path}#{'/' if it.type == 'tree'}" }
|
62
|
-
(["./"] + paths).sort
|
63
|
+
@client.tree(@repo, branch, recursive: true).tree.map { "#{it.path}#{'/' if it.type == 'tree'}" }
|
63
64
|
end
|
64
65
|
|
65
66
|
def fetch_token_via_gh
|
@@ -73,6 +74,7 @@ module Cafeznik
|
|
73
74
|
|
74
75
|
def grep_files(pattern)
|
75
76
|
Log.debug "Searching for pattern '#{pattern}' within #{@repo}"
|
77
|
+
Log.fatal "We can't search for empty patterns. Please provide a pattern to search for" if pattern.empty?
|
76
78
|
results = @client.search_code("#{pattern} repo:#{@repo} in:file").items.map(&:path)
|
77
79
|
Log.debug "Found #{results.size} files matching pattern '#{pattern}' in #{@repo}"
|
78
80
|
results
|
@@ -80,6 +82,22 @@ module Cafeznik
|
|
80
82
|
Log.error "Error during search for pattern '#{pattern}': #{e.message}"
|
81
83
|
[]
|
82
84
|
end
|
85
|
+
|
86
|
+
def fetch_file_content_with_retry(path, retries = MAX_RETRIES, base_delay = BASE_DELAY)
|
87
|
+
content_data = @client.contents(@repo, path: path)[:content]
|
88
|
+
Base64.decode64(content_data)
|
89
|
+
rescue Octokit::TooManyRequests
|
90
|
+
handle_rate_limit(path, retries, base_delay)
|
91
|
+
end
|
92
|
+
|
93
|
+
def handle_rate_limit(path, retries, base_delay)
|
94
|
+
return raise if retries <= 0
|
95
|
+
|
96
|
+
delay = (base_delay**(MAX_RETRIES - retries + 1)) * (0.8 + (0.4 * rand))
|
97
|
+
Log.warn "Rate limit exceeded, waiting #{delay.round(1)} seconds..."
|
98
|
+
sleep(delay)
|
99
|
+
fetch_file_content_with_retry(path, retries - 1, base_delay)
|
100
|
+
end
|
83
101
|
end
|
84
102
|
end
|
85
103
|
end
|
data/lib/cafeznik/version.rb
CHANGED
data/lib/cafeznik.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cafeznik
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lem
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-17 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: base64
|
@@ -37,6 +37,20 @@ dependencies:
|
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '2.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: concurrent-ruby
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.3'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.3'
|
40
54
|
- !ruby/object:Gem::Dependency
|
41
55
|
name: faraday-multipart
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -121,6 +135,20 @@ dependencies:
|
|
121
135
|
- - "~>"
|
122
136
|
- !ruby/object:Gem::Version
|
123
137
|
version: '0.10'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: tty-progressbar
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0.18'
|
145
|
+
type: :runtime
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0.18'
|
124
152
|
- !ruby/object:Gem::Dependency
|
125
153
|
name: rspec
|
126
154
|
requirement: !ruby/object:Gem::Requirement
|
@@ -195,16 +223,16 @@ dependencies:
|
|
195
223
|
name: super_diff
|
196
224
|
requirement: !ruby/object:Gem::Requirement
|
197
225
|
requirements:
|
198
|
-
- - "
|
226
|
+
- - "~>"
|
199
227
|
- !ruby/object:Gem::Version
|
200
|
-
version: '0'
|
228
|
+
version: '0.15'
|
201
229
|
type: :development
|
202
230
|
prerelease: false
|
203
231
|
version_requirements: !ruby/object:Gem::Requirement
|
204
232
|
requirements:
|
205
|
-
- - "
|
233
|
+
- - "~>"
|
206
234
|
- !ruby/object:Gem::Version
|
207
|
-
version: '0'
|
235
|
+
version: '0.15'
|
208
236
|
- !ruby/object:Gem::Dependency
|
209
237
|
name: webmock
|
210
238
|
requirement: !ruby/object:Gem::Requirement
|
@@ -234,6 +262,7 @@ files:
|
|
234
262
|
- lib/cafeznik.rb
|
235
263
|
- lib/cafeznik/cli.rb
|
236
264
|
- lib/cafeznik/content.rb
|
265
|
+
- lib/cafeznik/help.rb
|
237
266
|
- lib/cafeznik/log.rb
|
238
267
|
- lib/cafeznik/selector.rb
|
239
268
|
- lib/cafeznik/sources.rb
|
@@ -261,7 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
261
290
|
- !ruby/object:Gem::Version
|
262
291
|
version: '0'
|
263
292
|
requirements: []
|
264
|
-
rubygems_version: 3.6.
|
293
|
+
rubygems_version: 3.6.2
|
265
294
|
specification_version: 4
|
266
295
|
summary: CLI tool for copying files to your clipboard en masse
|
267
296
|
test_files: []
|