diff --git a/README.md b/README.md index 4a1d32c..e208581 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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) --- @@ -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" @@ -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 @@ -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 ``` @@ -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 ``` diff --git a/config.json b/config.json index 17682c8..35efec8 100644 --- a/config.json +++ b/config.json @@ -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 - } \ No newline at end of file + } diff --git a/rpcp b/rpcp new file mode 100755 index 0000000..192c2a3 --- /dev/null +++ b/rpcp @@ -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" "$@" diff --git a/rpcp.ps1 b/rpcp.ps1 index ff0d36d..1d7a6c2 100644 --- a/rpcp.ps1 +++ b/rpcp.ps1 @@ -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. @@ -83,6 +87,9 @@ Param( [hashtable]$Replacements, + [Parameter()] + [switch] $IncludeHiddenFolders, + [Parameter()] [switch] $ShowCopiedFiles ) @@ -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 + } } } @@ -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 } @@ -260,6 +295,7 @@ $filesToCopy = Get-FilesToInclude ` -RepoRoot $repoPath ` -IgnoreFolders $ignoreFolders ` -IgnoreFiles $ignoreFiles ` + -IncludeHiddenFolders:$includeHiddenFolders ` -MaxFileSize $maxFileSize diff --git a/rpcp.sh b/rpcp.sh index ff50b02..08482be 100644 --- a/rpcp.sh +++ b/rpcp.sh @@ -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 @@ -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 @@ -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;; @@ -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" @@ -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 @@ -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 @@ -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 } @@ -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") @@ -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 @@ -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 diff --git a/tests/bash/rpcp.bats b/tests/bash/rpcp.bats index 6bef195..366edf2 100644 --- a/tests/bash/rpcp.bats +++ b/tests/bash/rpcp.bats @@ -16,9 +16,11 @@ load 'test_helper/bats-support/load' load 'test_helper/bats-assert/load' setup() { + PROJECT_ROOT="$PWD" + # ── ① disposable sandbox repo ────────────────────────────── TMP_REPO="$(mktemp -d)" - mkdir -p "$TMP_REPO/src" "$TMP_REPO/build" + mkdir -p "$TMP_REPO/src" "$TMP_REPO/build" "$TMP_REPO/.codex/prompts" # source files printf 'hello ClientName\n' >"$TMP_REPO/src/include.txt" @@ -30,6 +32,7 @@ setup() { # something inside an ignored folder printf 'ignore me\n' >"$TMP_REPO/build/output.txt" + printf 'hidden codex notes\n' >"$TMP_REPO/.codex/prompts/agent.md" # config.json that matches the Pester suite cat >"$TMP_REPO/config.json" <<'JSON' @@ -66,6 +69,12 @@ run_rpcp() { CLIP="$(cat "$CLIP_FILE")" } +run_rpcp_launcher() { + local repo_path="$1" + run env PATH="$PROJECT_ROOT:$STUB_DIR:$PATH" bash -c "cd '$repo_path' && rpcp" + CLIP="$(cat "$CLIP_FILE")" +} + # ───────────────────────────────────────────────────────────── @test "default run: copies only permitted files & replaces tokens" { run_rpcp --repo-path "$TMP_REPO" --config-file "$TMP_REPO/config.json" @@ -81,6 +90,7 @@ run_rpcp() { refute_regex "$CLIP" "config\\.json" refute_regex "$CLIP" "output\\.txt" refute_regex "$CLIP" "big\\.bin" + refute_regex "$CLIP" "\\.codex/prompts/agent\\.md" } @test "size filter: big.bin is excluded by default" { @@ -100,6 +110,20 @@ run_rpcp() { refute_regex "$CLIP" "build/output\\.txt" } +@test "hidden folders are excluded by default" { + run_rpcp --repo-path "$TMP_REPO" --config-file "$TMP_REPO/config.json" + refute_regex "$CLIP" "\\.codex/prompts/agent\\.md" +} + +@test "--include-hidden-folders includes dot-prefixed directories" { + run_rpcp --repo-path "$TMP_REPO" \ + --config-file "$TMP_REPO/config.json" \ + --include-hidden-folders + + assert_regex "$CLIP" "\\.codex/prompts/agent\\.md" + assert_regex "$CLIP" "hidden codex notes" +} + @test "--show-copied-files does not affect clipboard content" { run_rpcp --repo-path "$TMP_REPO" \ --config-file "$TMP_REPO/config.json" \ @@ -109,3 +133,30 @@ run_rpcp() { # we just need to ensure normal data is still on the clipboard assert_regex "$CLIP" "include\\.txt" } + +@test "rpcp launcher prefers the current directory config.json" { + run_rpcp_launcher "$TMP_REPO" + + assert_success + assert_regex "$CLIP" "include\\.txt" + assert_regex "$CLIP" "hello Bob" + refute_regex "$CLIP" "\\.codex/prompts/agent\\.md" + refute_regex "$CLIP" "manifest\\.json" + refute_regex "$CLIP" "config\\.json" +} + +@test "rpcp launcher falls back to the bundled config when local config.json is missing" { + local no_config_repo + no_config_repo="$(mktemp -d)" + trap 'rm -rf "$no_config_repo"' RETURN + + printf 'hello from launcher\n' >"$no_config_repo/include.txt" + printf 'ignore me\n' >"$no_config_repo/manifest.json" + + run_rpcp_launcher "$no_config_repo" + + assert_success + assert_regex "$CLIP" "include\\.txt" + assert_regex "$CLIP" "hello from launcher" + refute_regex "$CLIP" "manifest\\.json" +} diff --git a/tests/fixtures/sample-repo/.codex/prompts/agent.md b/tests/fixtures/sample-repo/.codex/prompts/agent.md new file mode 100644 index 0000000..177a638 --- /dev/null +++ b/tests/fixtures/sample-repo/.codex/prompts/agent.md @@ -0,0 +1 @@ +hidden codex notes diff --git a/tests/powershell/rpcp.tests.ps1 b/tests/powershell/rpcp.tests.ps1 index a7b5193..66a0317 100644 --- a/tests/powershell/rpcp.tests.ps1 +++ b/tests/powershell/rpcp.tests.ps1 @@ -30,7 +30,14 @@ Describe 'rpcp.ps1 end-to-end behaviour (fixture repo)' { if (-not (Test-Path "$FixtureRoot/image.png")) { [IO.File]::WriteAllBytes("$FixtureRoot/image.png",[byte[]](0..9)) } - @' + if (-not (Test-Path "$FixtureRoot/.codex/prompts")) { + New-Item -ItemType Directory -Path "$FixtureRoot/.codex/prompts" -Force | Out-Null + } + if (-not (Test-Path "$FixtureRoot/.codex/prompts/agent.md")) { + 'hidden codex notes' | Set-Content "$FixtureRoot/.codex/prompts/agent.md" + } + if (-not (Test-Path "$FixtureRoot/config.json")) { + @' { "repoPath": ".", "maxFileSize": 204800, @@ -40,6 +47,7 @@ Describe 'rpcp.ps1 end-to-end behaviour (fixture repo)' { "showCopiedFiles": false } '@ | Set-Content "$FixtureRoot/config.json" + } # -- extra files for later tests ----------------------------------- if (-not (Test-Path "$FixtureRoot/src/big.bin")) { $size = 300kb # 300 KiB > maxFileSize @@ -96,6 +104,29 @@ function Invoke-Rpcp { $copied | Should -Not -Match 'config\.json' $copied | Should -Not -Match 'output\.txt' $copied | Should -Not -Match 'big\.bin' + $copied | Should -Not -Match '\.codex[\\/]+prompts[\\/]+agent\.md' + } + } + + Context 'Hidden folder handling' { + It 'excludes dot-prefixed folders by default' { + $copied = Invoke-Rpcp -Param @{ + RepoPath = $FixtureRoot + ConfigFile = "$FixtureRoot/config.json" + } + + $copied | Should -Not -Match '\.codex[\\/]+prompts[\\/]+agent\.md' + } + + It 'includes dot-prefixed folders when -IncludeHiddenFolders is used' { + $copied = Invoke-Rpcp -Param @{ + RepoPath = $FixtureRoot + ConfigFile = "$FixtureRoot/config.json" + IncludeHiddenFolders = $true + } + + $copied | Should -Match '\.codex[\\/]+prompts[\\/]+agent\.md' + $copied | Should -Match 'hidden codex notes' } }