Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ rpcp --repo-path ~/src/my-project --verbose
```

> **Clipboard helpers** –
> • Linux: requires `xclip`
> • Linux: uses `xclip`, `wl-copy`, `xsel`, or OSC52 terminal clipboard support
> • macOS: uses `pbcopy`
> • WSL: uses `clip.exe`

Expand All @@ -74,7 +74,7 @@ rpcp --repo-path ~/src/my-project --verbose

* **Bash** 4+ **or** **PowerShell** 7+
* `jq` – for the Bash version (auto‑installed if `autoInstallDeps = true`)
* A clipboard tool (`pbcopy`, `xclip`, `clip.exe`, or `pwsh`)
* A clipboard tool (`pbcopy`, `xclip`, `wl-copy`, `xsel`, `clip.exe`, `pwsh`, or OSC52 terminal clipboard support)

---

Expand All @@ -86,12 +86,15 @@ rpcp --repo-path ~/src/my-project --verbose
"repoPath": ".",

// ignore folders / files (globs ok)
"ignoreFolders": ["build", ".git", "node_modules"],
"ignoreFolders": ["build", "node_modules"],
"ignoreFiles": ["manifest.json", "*.png"],

// max bytes per file (0 = unlimited)
"maxFileSize": 204800,

// include dot-prefixed folders like .codex and .git
"includeHiddenFolders": false,

// string replacements applied to every file (So in this case, ACME will be replaced with Company_name)
"replacements": {
"ACME": "Company_name"
Expand All @@ -108,6 +111,10 @@ rpcp --repo-path ~/src/my-project --verbose
Every CLI switch overrides the matching JSON field – handy when you just
need to bump `--max-file-size` for one run.

Dot-prefixed folders such as `.codex`, `.git`, and `.github` are excluded by
default. Set `includeHiddenFolders` to `true`, or use the CLI switch for a
one-off run, if you want them included.

---

## 💻 Usage snippets
Expand All @@ -121,6 +128,9 @@ rpcp
# disable size filter
rpcp -MaxFileSize 0

# include hidden folders like .codex for one run
rpcp -IncludeHiddenFolders

# different folder, quiet output
rpcp -RepoPath C:\Code\Foo -Verbose:$false
```
Expand All @@ -134,6 +144,9 @@ rpcp
# disable size filter & summary
rpcp --max-file-size 0 --show-copied-files=false

# include hidden folders like .codex for one run
rpcp --include-hidden-folders

# different folder
rpcp --repo-path ~/Code/Foo
```
Expand Down
5 changes: 3 additions & 2 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"repoPath": ".",
"maxFileSize": 204800,
"ignoreFolders": [".git", ".github", ".terraform", "node_modules","plugin-cache", "terraform-provider*", "logo-drafts","build", ".archive","test_helper","exclude_test"],
"includeHiddenFolders": false,
"ignoreFolders": ["node_modules", "plugin-cache", "terraform-provider*", "logo-drafts", "build", "test_helper", "exclude_test"],
"ignoreFiles": ["manifest.json", "package-lock.json","*.png","*.jpg","*.jpeg","*.gif","*.svg","*.zip","*.tar.gz","*.tgz","*.tfstate", "*.tfstate.backup", "*.tfvars", "*.tfvars.json", "*.tfplan", "*.tfplan.json","*.avif","*.webp"],
"replacements": { "ACME":"Company_name","Bob":"Redacted_name","Secret Project Name":"Redacted_project_name" },
"showCopiedFiles": true,
"autoInstallDeps": true
}
}
25 changes: 25 additions & 0 deletions rpcp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(
CDPATH= cd -- "$(dirname -- "$0")" && pwd
)"
RPCP_SCRIPT="$SCRIPT_DIR/rpcp.sh"
DEFAULT_CONFIG="$SCRIPT_DIR/config.json"

USE_EXPLICIT_CONFIG=0
for arg in "$@"; do
case "$arg" in
--config-file|--config-file=*)
USE_EXPLICIT_CONFIG=1
break
;;
esac
done

if [[ $USE_EXPLICIT_CONFIG -eq 1 || -f "$PWD/config.json" ]]; then
exec bash "$RPCP_SCRIPT" "$@"
fi

exec bash "$RPCP_SCRIPT" --config-file "$DEFAULT_CONFIG" "$@"
54 changes: 45 additions & 9 deletions rpcp.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
Hashtable of token → replacement pairs.
Overrides the config file’s replacements when specified.

.PARAMETER IncludeHiddenFolders
Includes files inside dot-prefixed folders such as .codex and .git.
By default hidden folders are excluded.

.PARAMETER ShowCopiedFiles
If specified (or if “showCopiedFiles” is true in config.json), after copying
to clipboard the script will list every file that was included.
Expand Down Expand Up @@ -83,6 +87,9 @@ Param(

[hashtable]$Replacements,

[Parameter()]
[switch] $IncludeHiddenFolders,

[Parameter()]
[switch] $ShowCopiedFiles
)
Expand Down Expand Up @@ -115,26 +122,47 @@ function Get-FilesToInclude {
[Parameter()]
[string[]] $IgnoreFiles = @(),

[Parameter()]
[switch] $IncludeHiddenFolders,

[Parameter()]

[ValidateRange(0, [long]::MaxValue)]
[long] $MaxFileSize
)
$allFiles = Get-ChildItem -Path (Join-Path $RepoRoot '*') -Recurse -File -ErrorAction Stop
$getChildItemParams = @{
Path = (Join-Path $RepoRoot '*')
Recurse = $true
File = $true
ErrorAction = 'Stop'
}
if ($IncludeHiddenFolders) {
$getChildItemParams.Force = $true
}

$allFiles = Get-ChildItem @getChildItemParams
$result = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
$resolvedRepoRoot = (Resolve-Path -LiteralPath $RepoRoot).Path

foreach ($f in $allFiles) {
$reason = $null
$relativePath = [IO.Path]::GetRelativePath($resolvedRepoRoot, $f.FullName)
$relativeDirectory = [IO.Path]::GetDirectoryName($relativePath)
$relativeDirSegments = if ($relativeDirectory) { @($relativeDirectory -split '[\\/]') } else { @() }

# Folder pattern check
if (-not $reason -and -not $IncludeHiddenFolders) {
if ($relativeDirSegments | Where-Object { $_ -like '.*' -and $_ -ne '.' -and $_ -ne '..' }) {
$reason = 'hidden folder excluded by default'
}
}

$dirs = $f.DirectoryName.Split([IO.Path]::DirectorySeparatorChar)
foreach ($folderPattern in $IgnoreFolders) {
$sepRegex = [Regex]::Escape([IO.Path]::DirectorySeparatorChar)
$segments = $f.DirectoryName -split $sepRegex # safe on Win & *nix
if ($segments -like $folderPattern) {
$reason = "matched ignore-folder '$folderPattern'"
break
# Folder pattern check
if (-not $reason) {
foreach ($folderPattern in $IgnoreFolders) {
if ($relativeDirSegments -like $folderPattern) {
$reason = "matched ignore-folder '$folderPattern'"
break
}
}
}

Expand Down Expand Up @@ -243,6 +271,13 @@ $repoPath = if ($PSBoundParameters.ContainsKey('RepoPath')) { $RepoPath } else {
$maxFileSize = if ($PSBoundParameters.ContainsKey('MaxFileSize')) { $MaxFileSize } else { [long]$config.maxFileSize }
$ignoreFolders = if ($PSBoundParameters.ContainsKey('IgnoreFolders') -and $IgnoreFolders) { $IgnoreFolders } else { @($config.ignoreFolders) }
$ignoreFiles = if ($PSBoundParameters.ContainsKey('IgnoreFiles') -and $IgnoreFiles) { $IgnoreFiles } else { @($config.ignoreFiles) }
$includeHiddenFolders = if ($PSBoundParameters.ContainsKey('IncludeHiddenFolders')) {
$IncludeHiddenFolders.IsPresent
} elseif ($null -ne $config.includeHiddenFolders) {
[bool]$config.includeHiddenFolders
} else {
$false
}
$replacements = if ($PSBoundParameters.ContainsKey('Replacements')) { $Replacements } else {
$tempHashtable = @{}; foreach ($p in $config.replacements.PSObject.Properties) { $tempHashtable[$p.Name] = $p.Value }; $tempHashtable
}
Expand All @@ -260,6 +295,7 @@ $filesToCopy = Get-FilesToInclude `
-RepoRoot $repoPath `
-IgnoreFolders $ignoreFolders `
-IgnoreFiles $ignoreFiles `
-IncludeHiddenFolders:$includeHiddenFolders `
-MaxFileSize $maxFileSize


Expand Down
46 changes: 41 additions & 5 deletions rpcp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# rpcp.sh [--repo-path path] [--config-file file]
# [--max-file-size bytes] [--ignore-folders pat1,pat2]
# [--ignore-files f1,f2] [--replacements '{"T":"v",...}']
# [--show-copied-files] [--verbose]
# [--include-hidden-folders] [--show-copied-files] [--verbose]
#
# Example:
# rpcp.sh --verbose
Expand Down Expand Up @@ -94,7 +94,7 @@ usage() {
# Parse CLI arguments
parse_args() {
local opts
opts=$(getopt -o h --long help,repo-path:,config-file:,max-file-size:,ignore-folders:,ignore-files:,replacements:,show-copied-files,verbose -- "$@")
opts=$(getopt -o h --long help,repo-path:,config-file:,max-file-size:,ignore-folders:,ignore-files:,replacements:,include-hidden-folders,show-copied-files,verbose -- "$@")
eval set -- "$opts"
while true; do
case "$1" in
Expand All @@ -104,6 +104,7 @@ parse_args() {
--ignore-folders) CLI_IGNORE_FOLDERS="$2"; shift 2;;
--ignore-files) CLI_IGNORE_FILES="$2"; shift 2;;
--replacements) CLI_REPLACEMENTS_JSON="$2"; shift 2;;
--include-hidden-folders) CLI_INCLUDE_HIDDEN_FOLDERS="true"; shift;;
--show-copied-files) CLI_SHOW_COPIED_FILES="true"; shift;;
--verbose) VERBOSE=1; shift;;
-h|--help) usage;;
Expand All @@ -118,6 +119,11 @@ csv_to_array() {
local IFS=','; read -r -a arr <<< "$1"; echo "${arr[@]}"
}

is_truthy() {
local value="${1:-}"
[[ "${value,,}" == "true" || "$value" == "1" ]]
}

# Decide whether to include a file; log if verbose
should_include() {
local file="$1"
Expand Down Expand Up @@ -156,7 +162,11 @@ should_include() {

# Collect files to include
collect_files() {
mapfile -t ALL_FILES < <(find "$REPO_PATH" -type f)
if is_truthy "$INCLUDE_HIDDEN_FOLDERS"; then
mapfile -t ALL_FILES < <(find "$REPO_PATH" -type f)
else
mapfile -t ALL_FILES < <(find "$REPO_PATH" \( -type d -name '.*' ! -path "$REPO_PATH" \) -prune -o -type f -print)
fi
INCLUDED_FILES=()
for f in "${ALL_FILES[@]}"; do
if should_include "$f"; then
Expand All @@ -183,6 +193,24 @@ build_content() {
done
}

# Best-effort terminal clipboard fallback for environments without a native
# clipboard command installed.
copy_to_osc52() {
local content="$1"
local encoded

command -v base64 &>/dev/null || return 1
[[ -w /dev/tty ]] || return 1

encoded=$(printf '%s' "$content" | base64 | tr -d '\r\n')

if [[ -n "${TMUX:-}" ]]; then
printf '\033Ptmux;\033\033]52;c;%s\a\033\\' "$encoded" > /dev/tty
else
printf '\033]52;c;%s\a' "$encoded" > /dev/tty
fi
}

# Copy STDIN to clipboard on macOS, Linux, WSL
copy_to_clipboard() {
local content
Expand All @@ -191,12 +219,18 @@ copy_to_clipboard() {
printf '%s' "$content" | pbcopy
elif command -v xclip &>/dev/null; then
printf '%s' "$content" | xclip -selection clipboard
elif command -v wl-copy &>/dev/null; then
printf '%s' "$content" | wl-copy
elif command -v xsel &>/dev/null; then
printf '%s' "$content" | xsel --clipboard --input
elif command -v clip.exe &>/dev/null; then
printf '%s' "$content" | clip.exe
elif command -v powershell.exe &>/dev/null; then
printf '%s' "$content" | powershell.exe -NoProfile -Command "Set-Clipboard"
elif copy_to_osc52 "$content"; then
:
else
echo "Error: no clipboard utility found (pbcopy, xclip, clip.exe or powershell.exe)" >&2
echo "Error: no clipboard utility found (pbcopy, xclip, wl-copy, xsel, clip.exe, powershell.exe, or OSC52 terminal clipboard)" >&2
exit 3
fi
}
Expand Down Expand Up @@ -233,6 +267,7 @@ load_config() {
local cfg="$1"
REPO_PATH=$(jq -r '.repoPath // "."' "$cfg")
MAX_FILE_SIZE=$(jq -r '.maxFileSize // 0' "$cfg")
INCLUDE_HIDDEN_FOLDERS=$(jq -r '.includeHiddenFolders // false' "$cfg")
SHOW_COPIED_FILES=$(jq -r '.showCopiedFiles // false' "$cfg")
mapfile -t CFG_IGNORE_FOLDERS < <(jq -r '.ignoreFolders[]?' "$cfg")
mapfile -t CFG_IGNORE_FILES < <(jq -r '.ignoreFiles[]?' "$cfg")
Expand All @@ -247,6 +282,7 @@ load_config "$CONFIG_FILE"
# Merge CLI overrides
REPO_PATH=${CLI_REPO_PATH:-$REPO_PATH}
MAX_FILE_SIZE=${CLI_MAX_FILE_SIZE:-$MAX_FILE_SIZE}
INCLUDE_HIDDEN_FOLDERS=${CLI_INCLUDE_HIDDEN_FOLDERS:-$INCLUDE_HIDDEN_FOLDERS}
SHOW_COPIED_FILES=${CLI_SHOW_COPIED_FILES:-$SHOW_COPIED_FILES}

if [[ -n "${CLI_IGNORE_FOLDERS:-}" ]]; then
Expand Down Expand Up @@ -280,7 +316,7 @@ build_content | copy_to_clipboard

# Show summary
echo "✅ Copied ${#INCLUDED_FILES[@]} file(s) to clipboard."
if [[ $SHOW_COPIED_FILES == "true" || $SHOW_COPIED_FILES == "1" ]]; then
if is_truthy "$SHOW_COPIED_FILES"; then
echo
echo "Files included:"
for f in "${INCLUDED_FILES[@]}"; do
Expand Down
Loading
Loading