copy_code 0.3.0.beta

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: a4d333eaa0ad272d522245a8da8924bb4735f2579bf1820b5e4698ab8eeb1b99
4
+ data.tar.gz: b750c71bd022f0630007266b73a3f0781d693722d4ccfbca6c840b9c4137e00a
5
+ SHA512:
6
+ metadata.gz: 1c14cafc2fd57a0c6d32ce6b9e73c527909a245b9ea11e49be207e835618132db32a55ec52831f5fec29b385a9c9a2b533dbff6f1b924a583748ccbffd98fedf
7
+ data.tar.gz: a6456aa45e75e3529f623c0d4ae4b110623e0dbb939254f86ec1a0a1df56bacc97ac8ca6200f59946741c43e43b64336eb9af074b37400b6a2ca08f138b45444
data/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2025 t0nylombardi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # ๐Ÿง  copy_code
2
+
3
+ ![Gem Version](https://img.shields.io/gem/v/copy_code.svg)
4
+ ![Ruby](https://img.shields.io/badge/ruby-3.0%2B-red)
5
+ ![Build Status](https://img.shields.io/github/actions/workflow/status/t0nylombardi/copy_code/test.yml?branch=main)
6
+ ![Code Style](https://img.shields.io/badge/code%20style-rubocop-brightgreen)
7
+
8
+ A smart, flexible CLI tool to copy source code from a directory (or project) into your clipboard or a text file โ€” while skipping unnecessary files using `.ccignore`.
9
+
10
+ ---
11
+
12
+ ## ๐Ÿ”ง Features
13
+
14
+ - โœ… Recursively finds source code files in a target directory
15
+ - ๐Ÿงน Ignores folders like `.git`, `node_modules`, etc. using a `.copy_codeignore` file
16
+ - ๐ŸŽฏ Filters by file extension (`--extensions=rb,py,js`)
17
+ - ๐Ÿ“‹ Copies output to your clipboard or saves to a file (`--print=pbcopy|txt`)
18
+ - ๐Ÿš Works from the terminal with one clean command
19
+
20
+ ---
21
+
22
+ ### Options
23
+
24
+ | flags | options |
25
+ | -------------------- | -------------------------------------------------------------------- |
26
+ | -e, --extensions=EXT | Comma-separated list of file extensions to include (e.g. `-e rb,py`) |
27
+ | -p, --print=OUT | Output format: `pbcopy` (clipboard) or `txt` (file) |
28
+ | -h | Show help information |
29
+
30
+ ## ๐Ÿš€ Installation
31
+
32
+ ```sh
33
+ gem install copy_code
34
+ ```
35
+
36
+ Or clone and build manually:
37
+
38
+ ```sh
39
+ git clone https://github.com/your_username/copy_code.git
40
+ cd copy_code
41
+ gem build copy_code.gemspec
42
+ gem install ./copy_code-*.gem
43
+ ```
44
+
45
+ ## ๐Ÿ›  Usage
46
+
47
+ ```sh
48
+ copy_code [options] [paths]
49
+ ```
50
+
51
+ ### Examples
52
+
53
+ Copy all .rb and .py files from the current directory and subdirectories:
54
+
55
+ ```sh
56
+ copy_code -e rb,py
57
+ ```
58
+
59
+ Copy .js, .ts, and .json files from a specific folder and write to a file:
60
+
61
+ ```sh
62
+ copy_code ~/projects/my-app -e js,ts,json -p txt
63
+ ```
64
+
65
+ Copy all .rb and .py files from the current directory and subdirectories:
66
+
67
+ ```sh
68
+ copy_code -e rb,py
69
+ ```
70
+
71
+ Copy .js, .ts, and .json files from a specific folder and write to a file:
72
+
73
+ ```sh
74
+ copy_code ~/projects/my-app -e js,ts,json -p txt
75
+ ```
76
+
77
+ ```sh
78
+ copy_code -e rb,py
79
+ ```
80
+
81
+ Copy .js, .ts, and .json files from a specific folder and write to a file:
82
+
83
+ ```sh
84
+ copy_code ~/projects/my-app -e js,ts,json -p txt
85
+ ```
86
+
87
+ ```sh
88
+ copy_code -e rb,py
89
+ ```
90
+
91
+ ### ๐Ÿ“‚ .ccignore
92
+
93
+ You can create a `.ccignore` file in your project directory to specify files and directories to ignore. This works similarly to `.gitignore`.
94
+
95
+ ### Example `.ccignore` file
96
+
97
+ ```
98
+ # Ignore all node_modules directories
99
+ node_modules/
100
+ # Ignore all .git directories
101
+ .git/
102
+ # Ignore all log files
103
+ *.log
104
+ # Ignore all temporary files
105
+ *.tmp
106
+ # Ignore all coverage directories
107
+ coverage/
108
+ # Ignore all virtual environments
109
+ .venv/
110
+ ```
111
+
112
+ ## ๐Ÿ“œ License
113
+
114
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "code_copy"
6
+
7
+ require "irb"
8
+ IRB.start(__FILE__)
data/bin/copy_code ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require_relative "../lib/copy_code"
6
+
7
+ CopyCode::CLI::Runner.run(ARGV)
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # copy_code/lib/copy_code/cli/ignore_loader.rb
4
+ #
5
+ # This file defines the CopyCode::CLI::IgnoreLoader class, which is responsible
6
+ # for loading ignore patterns from a `.copy_codeignore` file.
7
+ #
8
+ # It checks the first project target path passed to the CLI, and falls back
9
+ # to the user's `~/.ccignore` if not found.
10
+
11
+ module CopyCode
12
+ module CLI
13
+ # IgnoreLoader loads a list of path substrings from a `.ccignore` file.
14
+ # It will attempt to read from the first provided target path.
15
+ #
16
+ # If no ignore file exists in that path, it will fall back to ~/.ccignore.
17
+ class IgnoreLoader
18
+ # @param ignore_patterns [Array<String>] substrings to match in file paths
19
+ def initialize(ignore_patterns = [])
20
+ @patterns = ignore_patterns || []
21
+ end
22
+
23
+ # Loads ignore patterns from a given target directory or fallback path.
24
+ #
25
+ # @param target_path [String] the root of the scanned project
26
+ # @return [Array<String>] a list of path substrings to ignore
27
+ def self.load(target_path)
28
+ local_file = File.join(target_path, ".ccignore")
29
+ fallback_file = File.expand_path("~/.ccignore")
30
+
31
+ file = File.exist?(local_file) ? local_file : fallback_file
32
+ return [] unless File.exist?(file)
33
+
34
+ lines = File.readlines(file).map(&:strip)
35
+ lines.reject!(&:empty?)
36
+ lines || []
37
+ end
38
+
39
+ # @param file [String]
40
+ # @return [Boolean] whether the file should be excluded
41
+ def exclude?(file)
42
+ @patterns.any? { |pattern| file.include?("/#{pattern}/") }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file defines the CopyCode::CLI::OutputWriter class, which is responsible
4
+ # for outputting formatted code content to either:
5
+ # - The clipboard (using macOS `pbcopy`)
6
+ # - A text file named `code_output.txt`
7
+ #
8
+ # Output method is selected via CLI flag `-p txt|pbcopy`.
9
+
10
+ module CopyCode
11
+ module CLI
12
+ # OutputWriter handles writing the final formatted code string
13
+ # to the destination selected by the user (`pbcopy` or `txt`).
14
+ #
15
+ # If `txt` is specified, the content is saved to `code_output.txt`.
16
+ # If no method or `pbcopy` is specified, the content is copied to the clipboard
17
+ # using macOS `pbcopy` via `IO.popen`.
18
+ class OutputWriter
19
+ # Writes the given content to the selected output method.
20
+ #
21
+ # @param content [String] the fully formatted output content
22
+ # @param method [String] the output method ("txt" or "pbcopy")
23
+ # @return [void]
24
+ def self.write(content, method)
25
+ case method
26
+ when "txt"
27
+ File.write("code_output.txt", content)
28
+ puts "โœ… Saved to code_output.txt"
29
+ else
30
+ IO.popen("pbcopy", "w") { |io| io.write(content) }
31
+ puts "โœ… Copied to clipboard"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module CopyCode
6
+ module CLI
7
+ # Parser handles CLI argument parsing and validation for the CopyCode CLI tool.
8
+ #
9
+ # It processes flags like:
10
+ # - `-e` to specify file extensions
11
+ # - `-p` to control output format (clipboard or text file)
12
+ # and any paths passed to the command.
13
+ #
14
+ # Usage:
15
+ # parser = CopyCode::CLI::Parser.new(ARGV)
16
+ # options = parser.parse
17
+ #
18
+ # Returns an options hash:
19
+ # {
20
+ # extensions: ['rb', 'py'],
21
+ # output: 'txt',
22
+ # targets: ['/full/path/to/project']
23
+ # }
24
+ class Parser
25
+ # @param argv [Array<String>] The raw arguments passed from the command line
26
+ def initialize(argv)
27
+ @argv = argv
28
+ @options = {
29
+ extensions: [],
30
+ output: "pbcopy",
31
+ targets: []
32
+ }
33
+ end
34
+
35
+ # Parses the provided arguments and returns a validated options hash.
36
+ #
37
+ # @return [Hash] Parsed and validated options including :extensions, :output, and :targets
38
+ def parse
39
+ build_parser.parse!(@argv)
40
+ assign_targets
41
+ validate_targets
42
+ @options
43
+ end
44
+
45
+ private
46
+
47
+ # Builds the OptionParser instance that parses CLI flags.
48
+ #
49
+ # @return [OptionParser]
50
+ def build_parser
51
+ OptionParser.new do |opts|
52
+ opts.banner = "Usage: copy_code [options] [paths]"
53
+ opts.on("-eEXT", "--extensions=EXT", "Comma-separated list (e.g. rb,py,js)") do |ext|
54
+ @options[:extensions] = parse_extensions(ext)
55
+ end
56
+ opts.on("-pOUT", "--print=OUT", "Output method: pbcopy (default) or txt") do |out|
57
+ @options[:output] = out
58
+ end
59
+ end
60
+ end
61
+
62
+ # Parses the extensions string passed from the CLI flag.
63
+ #
64
+ # @param raw [String] Comma-separated list of extensions
65
+ # @return [Array<String>] Cleaned list of extensions
66
+ def parse_extensions(raw)
67
+ raw.split(",").map(&:strip)
68
+ end
69
+
70
+ # Expands and assigns the target paths passed as positional args.
71
+ #
72
+ # @return [void]
73
+ def assign_targets
74
+ @options[:targets] = @argv unless @argv.empty?
75
+ @options[:targets].map! { |t| File.expand_path(t) }
76
+ end
77
+
78
+ # Validates each target path and warns if any don't exist.
79
+ #
80
+ # @return [void]
81
+ def validate_targets
82
+ @options[:targets].each do |path|
83
+ warn "[WARN] Target path does not exist: #{path}" unless Dir.exist?(path)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ # copy_code/lib/copy_code/cli/runner.rb
4
+ #
5
+ # This file defines the CopyCode::CLI::Runner class, which is the core orchestrator
6
+ # for the command-line execution of the copy_code tool.
7
+ #
8
+ # The Runner is responsible for:
9
+ # - Parsing CLI arguments
10
+ # - Loading ignore patterns
11
+ # - Constructing file filters
12
+ # - Discovering and formatting files
13
+ # - Writing the result to either clipboard or a text file
14
+ #
15
+ # It separates the CLI runtime flow from the CLI namespace (CopyCode::CLI),
16
+ # following SOLID and DDD principles.
17
+
18
+ module CopyCode
19
+ module CLI
20
+ # Runner executes the core command flow of the CopyCode CLI.
21
+ #
22
+ # It acts as a controller between CLI argument parsing,
23
+ # file discovery, formatting, and output.
24
+ class Runner
25
+ # @param argv [Array<String>] the raw arguments passed from the command line
26
+ def initialize(argv)
27
+ @argv = argv
28
+ end
29
+
30
+ def self.run(argv)
31
+ new(argv).execute
32
+ end
33
+
34
+ # Executes the complete CLI flow: parse โ†’ filter โ†’ format โ†’ output.
35
+ #
36
+ # @return [void]
37
+ def execute
38
+ OutputWriter.write(result, options[:output])
39
+ end
40
+
41
+ private
42
+
43
+ # Constructs all filters to apply during file discovery.
44
+ #
45
+ # @return [Array<#exclude?>] array of filter objects
46
+ def filters
47
+ @filters ||= [
48
+ Filters::FileExtensionFilter.new(options[:extensions]),
49
+ Filters::IgnorePathFilter.new(IgnoreLoader.load(options[:targets].first))
50
+ ]
51
+ end
52
+
53
+ # Creates the core file discovery/formatting engine.
54
+ #
55
+ # @return [CopyCode::Core]
56
+ def core
57
+ @core ||= Core.new(
58
+ targets: options[:targets],
59
+ filters: filters
60
+ )
61
+ end
62
+
63
+ # Retrieves the list of matching files from the core engine.
64
+ #
65
+ # @return [Array<String>] list of file paths
66
+ def files
67
+ @files ||= core.gather_files
68
+ end
69
+
70
+ # Formats the gathered files into a single output string.
71
+ #
72
+ # @return [String] formatted code content
73
+ def result
74
+ @result ||= core.format(files)
75
+ end
76
+
77
+ # Parses the command-line arguments using the CLI parser.
78
+ #
79
+ # @return [Hash] parsed options hash
80
+ def options
81
+ @options ||= Parser.new(@argv).parse
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pry"
4
+
5
+ module CopyCode
6
+ # Core file aggregator and formatter for the CopyCode domain
7
+ class Core
8
+ # @param targets [Array<String>] directories or files to scan
9
+ # @param filters [Array<#exclude?>] list of objects responding to `exclude?(file)`
10
+ def initialize(targets:, filters: [])
11
+ @targets = targets.empty? ? ["."] : targets
12
+ @filters = filters
13
+ end
14
+
15
+ # Gathers all matching files from the given targets.
16
+ #
17
+ # @return [Array<String>] list of absolute file paths
18
+ def gather_files
19
+ @targets.flat_map do |target|
20
+ Dir.glob("#{target}/**/*", File::FNM_DOTMATCH).select do |file|
21
+ is_file = File.file?(file)
22
+ is_included = include_file?(file)
23
+ is_file && is_included
24
+ end
25
+ end
26
+ end
27
+
28
+ # Formats the given files into output chunks with headers
29
+ #
30
+ # @param files [Array<String>] list of file paths
31
+ # @return [String] formatted text
32
+ def format(files)
33
+ files.map do |file|
34
+ header = "=== #{file} ==="
35
+ content = File.read(file)
36
+ "#{header}\n#{content}\n"
37
+ end.join("\n")
38
+ end
39
+
40
+ private
41
+
42
+ def include_file?(file)
43
+ @filters.none? { |f| f.exclude?(file) }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CopyCode
4
+ module Filters
5
+ # Filters files by allowed extension
6
+ class FileExtensionFilter
7
+ # @param extensions [Array<String>] file extensions without dot (e.g., "rb", "js")
8
+ def initialize(extensions)
9
+ @extensions = extensions.map { |ext| ".#{ext.gsub(/^\./, "")}" }
10
+ end
11
+
12
+ # @param file [String]
13
+ # @return [Boolean] whether the file should be excluded
14
+ def exclude?(file)
15
+ return false if @extensions.empty?
16
+
17
+ !@extensions.any? { |ext| file.end_with?(ext) }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CopyCode
4
+ module Filters
5
+ # Filters files by path substrings (e.g., ".venv", "node_modules")
6
+ class IgnorePathFilter
7
+ # @param ignore_patterns [Array<String>] substrings to match in file paths
8
+ def initialize(ignore_patterns)
9
+ @patterns = ignore_patterns
10
+ end
11
+
12
+ # @param file [String]
13
+ # @return [Boolean] whether the file should be excluded
14
+ def exclude?(file)
15
+ @patterns.any? { |pattern| file.include?("/#{pattern}/") }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CopyCode
4
+ VERSION = "0.3.0.beta"
5
+ end
data/lib/copy_code.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "copy_code/version"
4
+ require_relative "copy_code/cli"
5
+ require_relative "copy_code/core"
6
+
7
+ require_relative "copy_code/filters/file_extension_filter"
8
+ require_relative "copy_code/filters/ignore_path_filter"
9
+ require_relative "copy_code/cli/ignore_loader"
10
+ require_relative "copy_code/cli/output_writer"
11
+ require_relative "copy_code/cli/parser"
12
+
13
+ module CopyCode
14
+ # maybe some top-level stuff here
15
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: copy_code
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0.beta
5
+ platform: ruby
6
+ authors:
7
+ - t0nylombardi
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Find and copy code files from a project, with ignore rules and output
13
+ options
14
+ email:
15
+ - alombardi.331@gmail.com
16
+ executables:
17
+ - console
18
+ - copy_code
19
+ - setup
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - LICENSE
24
+ - README.md
25
+ - Rakefile
26
+ - bin/console
27
+ - bin/copy_code
28
+ - bin/setup
29
+ - lib/copy_code.rb
30
+ - lib/copy_code/cli.rb
31
+ - lib/copy_code/cli/ignore_loader.rb
32
+ - lib/copy_code/cli/output_writer.rb
33
+ - lib/copy_code/cli/parser.rb
34
+ - lib/copy_code/core.rb
35
+ - lib/copy_code/filters/file_extension_filter.rb
36
+ - lib/copy_code/filters/ignore_path_filter.rb
37
+ - lib/copy_code/version.rb
38
+ homepage: https://github.com/t0nylombardi/copy_code
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ homepage_uri: https://github.com/t0nylombardi/copy_code
43
+ source_code_uri: https://github.com/t0nylombardi/copy_code
44
+ changelog_uri: https://github.com/t0nylombardi/copy_code/blob/main/CHANGELOG.md
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.2.0
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.7.1
60
+ specification_version: 4
61
+ summary: CLI tool to copy code from a directory
62
+ test_files: []