# Codebase Export - codebase-export

## Structure

```
scripts/export.sh
SKILL.md
```

## Files

### scripts/export.sh

``````bash
#!/usr/bin/env bash
#
# codebase-export.sh
#
# Two modes:
#   1. Folder mode:    export.sh /path/to/folder
#   2. File-list mode: export.sh --files file1.js src/*.ts "path with spaces.md"
#
# IMPORTANT for file-list mode:
#   - Globs must be UNQUOTED so the shell expands them: src/*.ts
#   - Paths with spaces must be QUOTED: "docs/my file.md"
#
# Output: /workspace/downloads/<name>_export.md
# Returns: Download URL at tonki.dev
#

set -euo pipefail

###############################################################################
# Configuration
###############################################################################

OUTPUT_DIR="/workspace/downloads"
MAX_FILE_BYTES=$((512 * 1024))  # 512 KiB max per file

# Folders to ignore - EXACT match (except . and @ which are prefix-based)
IGNORE_FOLDERS_EXACT=(
  "node_modules"
  "__pycache__"
  ".git"
  ".vscode"
  ".idea"
  "dist"
  "build"
  ".next"
  "coverage"
  ".cache"
  "vendor"
  ".svn"
  ".hg"
)

# Folders to ignore - PREFIX match
IGNORE_FOLDERS_PREFIX=(
  "."   # Hidden folders
  "@"   # Special folders like @types
)

# File extensions to exclude content (binary/media)
EXCLUDED_EXTENSIONS=(
  "mp3" "wav" "ogg" "flac" "aac" "m4a"
  "png" "ico" "jpg" "jpeg" "svg" "gif" "webp" "bmp" "tiff" "psd"
  "mp4" "avi" "mov" "mkv" "webm" "flv"
  "pdf" "doc" "docx" "xls" "xlsx" "ppt" "pptx"
  "zip" "tar" "gz" "rar" "7z" "bz2" "xz"
  "dmg" "exe" "dll" "so" "dylib" "bin" "msi"
  "woff" "woff2" "ttf" "eot" "otf"
  "sqlite" "db" "sqlite3"
  "lock"
  "wasm" "class" "pyc" "pyo" "o" "a"
  "jar" "war" "ear"
  "iso" "img"
)

# Sensitive files - exclude content by basename
SENSITIVE_BASENAMES=(
  ".env"
  ".env.local"
  ".env.development"
  ".env.production"
  ".env.test"
  ".npmrc"
  ".pypirc"
  ".netrc"
  ".docker/config.json"
  "id_rsa"
  "id_ed25519"
  "id_ecdsa"
  "id_dsa"
  "credentials"
  "credentials.json"
  "service-account.json"
  "secrets.json"
  "secrets.yaml"
  "secrets.yml"
  ".htpasswd"
  "shadow"
  "passwd"
)

# Sensitive extensions - exclude content
SENSITIVE_EXTENSIONS=(
  "pem"
  "key"
  "p12"
  "pfx"
  "crt"
  "cer"
  "keystore"
  "jks"
)

###############################################################################
# Counters for summary
###############################################################################
COUNT_INCLUDED=0
COUNT_BINARY=0
COUNT_SENSITIVE=0
COUNT_TOO_LARGE=0
COUNT_NON_TEXT=0

###############################################################################
# Helper Functions
###############################################################################

# Portable realpath
realpath_compat() {
  local path="$1"
  if command -v realpath >/dev/null 2>&1; then
    realpath "$path"
  elif command -v greadlink >/dev/null 2>&1; then
    greadlink -f "$path"
  elif command -v readlink >/dev/null 2>&1 && readlink -f "$path" >/dev/null 2>&1; then
    readlink -f "$path"
  else
    # Fallback: use cd/pwd
    if [[ -d "$path" ]]; then
      (cd "$path" && pwd)
    elif [[ -f "$path" ]]; then
      local dir=$(dirname "$path")
      local base=$(basename "$path")
      echo "$(cd "$dir" && pwd)/$base"
    else
      echo "$path"
    fi
  fi
}

# Check if folder should be ignored
should_ignore_folder() {
  local folder_name="$1"

  # Check prefix patterns (. and @)
  for pattern in "${IGNORE_FOLDERS_PREFIX[@]}"; do
    if [[ "$folder_name" == "$pattern"* ]]; then
      return 0
    fi
  done

  # Check exact match patterns
  for pattern in "${IGNORE_FOLDERS_EXACT[@]}"; do
    if [[ "$folder_name" == "$pattern" ]]; then
      return 0
    fi
  done

  return 1
}

# Check if path contains an ignored folder
path_contains_ignored_folder() {
  local filepath="$1"
  local IFS='/'
  read -ra parts <<< "$filepath"
  for part in "${parts[@]}"; do
    if should_ignore_folder "$part"; then
      return 0
    fi
  done
  return 1
}

# Check if file extension is in the excluded list
is_excluded_extension() {
  local filename="$1"
  local extension="${filename##*.}"
  extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]')

  for ext in "${EXCLUDED_EXTENSIONS[@]}"; do
    if [[ "$extension" == "$ext" ]]; then
      return 0
    fi
  done
  return 1
}

# Check if file is sensitive
is_sensitive_file() {
  local filepath="$1"
  local filename=$(basename "$filepath")
  local extension="${filename##*.}"
  extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]')

  # Check basename
  for name in "${SENSITIVE_BASENAMES[@]}"; do
    if [[ "$filename" == "$name" ]]; then
      return 0
    fi
  done

  # Check extension
  for ext in "${SENSITIVE_EXTENSIONS[@]}"; do
    if [[ "$extension" == "$ext" ]]; then
      return 0
    fi
  done

  return 1
}

# Check if file is text (not binary) using grep
is_text_file() {
  local filepath="$1"
  # grep -I skips binary files, -q is quiet
  # If file is text, grep returns 0; if binary, returns 1
  if grep -Iq . "$filepath" 2>/dev/null; then
    return 0  # Text file
  else
    return 1  # Binary or empty
  fi
}

# Get file size in bytes
get_file_size() {
  local filepath="$1"
  if [[ "$(uname)" == "Darwin" ]]; then
    stat -f%z "$filepath" 2>/dev/null || echo 0
  else
    stat -c%s "$filepath" 2>/dev/null || echo 0
  fi
}

# Get language hint for syntax highlighting
get_language_hint() {
  local filename="$1"
  local ext="${filename##*.}"
  ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')

  case "$ext" in
    js) echo "javascript" ;;
    ts) echo "typescript" ;;
    jsx) echo "jsx" ;;
    tsx) echo "tsx" ;;
    py) echo "python" ;;
    rb) echo "ruby" ;;
    go) echo "go" ;;
    rs) echo "rust" ;;
    java) echo "java" ;;
    kt|kts) echo "kotlin" ;;
    swift) echo "swift" ;;
    c|h) echo "c" ;;
    cpp|hpp|cc|cxx|hxx) echo "cpp" ;;
    cs) echo "csharp" ;;
    php) echo "php" ;;
    sh|bash|zsh) echo "bash" ;;
    sql) echo "sql" ;;
    html|htm) echo "html" ;;
    css) echo "css" ;;
    scss|sass) echo "scss" ;;
    less) echo "less" ;;
    json) echo "json" ;;
    yaml|yml) echo "yaml" ;;
    xml) echo "xml" ;;
    md|markdown) echo "markdown" ;;
    toml) echo "toml" ;;
    ini|cfg|conf) echo "ini" ;;
    dockerfile) echo "dockerfile" ;;
    makefile) echo "makefile" ;;
    vue) echo "vue" ;;
    svelte) echo "svelte" ;;
    graphql|gql) echo "graphql" ;;
    proto) echo "protobuf" ;;
    tf) echo "hcl" ;;
    lua) echo "lua" ;;
    r) echo "r" ;;
    scala) echo "scala" ;;
    clj|cljs) echo "clojure" ;;
    ex|exs) echo "elixir" ;;
    erl|hrl) echo "erlang" ;;
    hs) echo "haskell" ;;
    ml|mli) echo "ocaml" ;;
    pl|pm) echo "perl" ;;
    *) echo "" ;;
  esac
}

# Sanitize name for filename/URL
sanitize_name() {
  local name="$1"
  # Replace spaces and special chars with underscores, keep alphanumeric, dots, hyphens
  echo "$name" | sed 's/[^A-Za-z0-9._-]/_/g' | sed 's/__*/_/g' | sed 's/^_//;s/_$//'
}

###############################################################################
# Parse Arguments
###############################################################################

MODE=""
TARGET_DIR=""
CUSTOM_NAME=""
declare -a FILES=()

# Generate random 8-char alphanumeric ID
generate_random_id() {
  local id
  id=$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom 2>/dev/null | head -c 8)
  if [[ -z "$id" ]]; then
    # Fallback to timestamp-based ID
    id=$(date +%s%N | sha256sum | head -c 8)
  fi
  echo "$id"
}

show_usage() {
  echo "Usage:" >&2
  echo "  Folder mode:    $0 [--name export_name] /path/to/folder" >&2
  echo "  File-list mode: $0 --files [--name export_name] file1.js src/*.ts ..." >&2
  echo "" >&2
  echo "Options:" >&2
  echo "  --name <name>   Custom export name (required for file-list mode)" >&2
  echo "" >&2
  echo "IMPORTANT for file-list mode:" >&2
  echo "  - Globs must be UNQUOTED: src/*.ts" >&2
  echo "  - Paths with spaces must be QUOTED: \"docs/my file.md\"" >&2
  echo "  - Always provide --name to identify your export" >&2
}

if [[ $# -eq 0 ]]; then
  show_usage
  exit 1
fi

# Parse --name if it's the first argument
if [[ "$1" == "--name" ]]; then
  if [[ -z "${2:-}" ]]; then
    echo "Error: --name requires a value" >&2
    exit 1
  fi
  CUSTOM_NAME="$2"
  shift 2
fi

if [[ $# -eq 0 ]]; then
  show_usage
  exit 1
fi

if [[ "$1" == "--files" ]]; then
  MODE="files"
  shift

  # Check for --name after --files
  if [[ "${1:-}" == "--name" ]]; then
    if [[ -z "${2:-}" ]]; then
      echo "Error: --name requires a value" >&2
      exit 1
    fi
    CUSTOM_NAME="$2"
    shift 2
  fi

  # Each remaining arg is a file path (shell already expanded globs)
  for arg in "$@"; do
    if [[ -f "$arg" ]]; then
      FILES+=("$(realpath_compat "$arg")")
    else
      echo "Warning: '$arg' is not a valid file, skipping." >&2
    fi
  done

  if [[ ${#FILES[@]} -eq 0 ]]; then
    echo "Error: No valid files found." >&2
    exit 1
  fi

  # Use custom name, or generate random ID if not provided
  if [[ -n "$CUSTOM_NAME" ]]; then
    EXPORT_NAME=$(sanitize_name "$CUSTOM_NAME")
  else
    EXPORT_NAME="export_$(generate_random_id)"
    echo "Note: No --name provided, using random ID: $EXPORT_NAME" >&2
  fi
else
  MODE="folder"
  TARGET_DIR="$1"

  TARGET_DIR="$(realpath_compat "$TARGET_DIR")"

  if [[ ! -d "$TARGET_DIR" ]]; then
    echo "Error: '$TARGET_DIR' is not a valid directory." >&2
    exit 1
  fi

  # Use custom name, or folder name as default
  if [[ -n "$CUSTOM_NAME" ]]; then
    EXPORT_NAME=$(sanitize_name "$CUSTOM_NAME")
  else
    EXPORT_NAME=$(sanitize_name "$(basename "$TARGET_DIR")")
  fi
fi

###############################################################################
# Setup Output
###############################################################################

mkdir -p "$OUTPUT_DIR"
OUTPUT_FILE="${OUTPUT_DIR}/${EXPORT_NAME}_export.md"
> "$OUTPUT_FILE"

echo "Generating export: $OUTPUT_FILE" >&2
echo "" >&2

###############################################################################
# Process a single file and append to output
###############################################################################

process_file() {
  local file="$1"
  local rel_path="$2"
  local filename=$(basename "$file")
  local exclusion_reason=""

  echo "### $rel_path" >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"

  # Check exclusion reasons in order of priority
  if is_sensitive_file "$file"; then
    exclusion_reason="sensitive"
    ((COUNT_SENSITIVE++)) || true
  elif is_excluded_extension "$filename"; then
    exclusion_reason="binary/media"
    ((COUNT_BINARY++)) || true
  elif ! is_text_file "$file"; then
    exclusion_reason="non-text"
    ((COUNT_NON_TEXT++)) || true
  else
    local file_size=$(get_file_size "$file")
    if [[ $file_size -gt $MAX_FILE_BYTES ]]; then
      exclusion_reason="too-large"
      ((COUNT_TOO_LARGE++)) || true
    fi
  fi

  if [[ -n "$exclusion_reason" ]]; then
    echo "*[Content excluded: ${exclusion_reason}]*" >> "$OUTPUT_FILE"
  else
    local lang=$(get_language_hint "$filename")
    # Use 6 backticks to avoid breaking on files containing ```
    echo "\`\`\`\`\`\`$lang" >> "$OUTPUT_FILE"
    cat "$file" >> "$OUTPUT_FILE"
    # Ensure newline before closing fence
    echo "" >> "$OUTPUT_FILE"
    echo '``````' >> "$OUTPUT_FILE"
    ((COUNT_INCLUDED++)) || true
  fi

  echo "" >> "$OUTPUT_FILE"
}

###############################################################################
# Generate Export - File List Mode
###############################################################################

if [[ "$MODE" == "files" ]]; then
  echo "# Codebase Export - Selected Files" >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"
  echo "Exported ${#FILES[@]} files." >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"

  # Find common base path for relative paths
  COMMON_BASE=""
  for file in "${FILES[@]}"; do
    if [[ -z "$COMMON_BASE" ]]; then
      COMMON_BASE=$(dirname "$file")
    else
      while [[ "${file#$COMMON_BASE/}" == "$file" && "$COMMON_BASE" != "/" ]]; do
        COMMON_BASE=$(dirname "$COMMON_BASE")
      done
    fi
  done

  # Structure section - list relative paths
  echo "## Structure" >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"
  echo '```' >> "$OUTPUT_FILE"

  declare -a REL_FILES=()
  for file in "${FILES[@]}"; do
    if [[ "$COMMON_BASE" == "/" ]]; then
      rel="${file#/}"
    else
      rel="${file#$COMMON_BASE/}"
    fi
    REL_FILES+=("$rel")
    echo "$rel" >> "$OUTPUT_FILE"
  done

  echo '```' >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"

  # Files section
  echo "## Files" >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"

  for i in "${!FILES[@]}"; do
    file="${FILES[$i]}"
    rel="${REL_FILES[$i]}"
    echo "Processing: $rel" >&2
    process_file "$file" "$rel"
  done
fi

###############################################################################
# Generate Export - Folder Mode
###############################################################################

if [[ "$MODE" == "folder" ]]; then
  echo "# Codebase Export - $(basename "$TARGET_DIR")" >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"

  # Collect all files first (handles newlines in filenames)
  declare -a ALL_FILES=()
  declare -a ALL_REL_PATHS=()

  while IFS= read -r -d '' file; do
    rel="${file#$TARGET_DIR/}"

    # Skip ignored paths
    if path_contains_ignored_folder "$rel"; then
      continue
    fi

    ALL_FILES+=("$file")
    ALL_REL_PATHS+=("$rel")
  done < <(find "$TARGET_DIR" -type f -print0 | sort -z)

  # Structure section - list relative paths
  echo "## Structure" >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"
  echo '```' >> "$OUTPUT_FILE"

  for rel in "${ALL_REL_PATHS[@]}"; do
    echo "$rel" >> "$OUTPUT_FILE"
  done

  echo '```' >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"

  # Files section
  echo "## Files" >> "$OUTPUT_FILE"
  echo "" >> "$OUTPUT_FILE"

  for i in "${!ALL_FILES[@]}"; do
    file="${ALL_FILES[$i]}"
    rel="${ALL_REL_PATHS[$i]}"
    echo "Processing: $rel" >&2
    process_file "$file" "$rel"
  done
fi

###############################################################################
# Summary
###############################################################################

EXPORT_SIZE=$(ls -lh "$OUTPUT_FILE" | awk '{print $5}')
TOTAL_EXCLUDED=$((COUNT_BINARY + COUNT_SENSITIVE + COUNT_TOO_LARGE + COUNT_NON_TEXT))

echo "" >&2
echo "========================================" >&2
echo "Export complete!" >&2
echo "========================================" >&2
echo "Files with content included: $COUNT_INCLUDED" >&2
echo "Files content-excluded:" >&2
echo "  - Binary/media: $COUNT_BINARY" >&2
echo "  - Sensitive: $COUNT_SENSITIVE" >&2
echo "  - Too large (>${MAX_FILE_BYTES} bytes): $COUNT_TOO_LARGE" >&2
echo "  - Non-text: $COUNT_NON_TEXT" >&2
echo "Total excluded: $TOTAL_EXCLUDED" >&2
echo "Export size: $EXPORT_SIZE" >&2
echo "========================================" >&2
echo "" >&2
echo "Download URL: https://www.tonki.dev/downloads/${EXPORT_NAME}_export.md"

``````

### SKILL.md

``````markdown
---
name: codebase-export
description: Package files or folders for another AI to review. Use when the user says "package these files for another AI review", "export this codebase", "create an export for AI review", or similar requests involving preparing code for external AI analysis.
allowed-tools: Bash, Read, Edit, Glob, Grep
---

# Codebase Export Skill

Export code files to a single markdown document for AI review.

## When to Use

Trigger on phrases like:
- "package these files for another AI review"
- "export this folder/codebase"
- "create an export for [AI name] to review"
- "prepare these files for external review"

## How to Export

### Folder Mode (entire directory)

```bash
~/.claude/skills/codebase-export/scripts/export.sh /path/to/folder
~/.claude/skills/codebase-export/scripts/export.sh --name auth-review /path/to/folder
```

Exports all files in the folder. Uses folder name as export name by default, or custom `--name` if provided.

### File-List Mode (specific files)

**IMPORTANT: Always provide `--name` to identify the export!**

```bash
~/.claude/skills/codebase-export/scripts/export.sh --files --name auth-security-review src/auth/*.ts src/middleware/auth.js
```

**CRITICAL - Quote rules:**
- **Globs must be UNQUOTED** (so the shell expands them): `src/*.ts`
- **Paths with spaces must be QUOTED**: `"docs/my file.md"`
- **Always use `--name`** to give exports meaningful, identifiable names

If `--name` is omitted in file-list mode, a random 8-character ID is generated (e.g., `export_a3k9x2m1`) to prevent overwrites.

## Output

- Saved to: `/workspace/downloads/<name>_export.md`
- Returns a download URL: `https://www.tonki.dev/downloads/<name>_export.md`
- Prints summary: files included, files excluded (by reason), export size

## Safety Features

The script automatically:
- **Excludes sensitive file content**: `.env`, `id_rsa`, `*.pem`, `*.key`, credentials, etc.
- **Excludes binary/media content**: images, videos, archives, executables, fonts
- **Detects non-text files**: uses `grep -Iq` to catch binaries without known extensions
- **Enforces size limits**: files >512KB are marked as too-large
- **Ignores common junk folders**: `node_modules`, `dist`, `build`, `.git`, etc.

Files are still listed in the structure, but their content is excluded with a reason.

## After Export

1. **Open the download link** for the user: `~/bin/open <url>`
2. **Add reviewer context block** if requested (see below)

---

## REQUIRED: Add Reviewer Context Block

When the user wants an intro/outro for the reviewing AI, you MUST add a context block that:

1. **Clearly explains:**
   - What this codebase is (app/script/library/etc)
   - What the user is trying to accomplish
   - The environment/runtime assumptions (Node/Python/Bash, OS, deployment target)
   - The main entrypoints and where to start reading
   - Any known constraints, pain points, or suspected bugs
   - What the reviewer should prioritize (security, correctness, architecture, etc)

2. **Explicitly asks the reviewing AI to respond as:**
   - A structured, action-oriented plan
   - Written as instructions to an AI agent who will implement the changes
   - Precise, step-by-step, with commands/paths where relevant
   - Including edge cases + tests
   - Including a checklist of fixes, ordered by impact/risk

### Template to Prepend

Use the Edit tool to prepend this template (filled in) to the export file:

```markdown
# Context for External Reviewer

## What this is
- **Project type:** [app/library/script/CLI tool/etc]
- **What it does:** [1-2 sentence description]
- **Target environment:** [Node.js/Python/Bash/Browser/etc, OS assumptions]

## How it's supposed to work
- **Primary flow:** [main user journey or execution path]
- **Key entrypoints:** [main files to start reading]
- **Inputs/outputs:** [what goes in, what comes out]

## What we need from you

Please respond with:
1. **Findings** (bugs, edge cases, security concerns)
2. **Fix plan** written as **instructions for an AI agent** (step-by-step, action-oriented)
3. **Patch strategy** (what files to change, what to add/remove)
4. **Tests / verification steps** (exact commands + expected outcomes)
5. **Risk notes** (what could break / rollout concerns)

## Constraints / preferences
- Be precise and practical.
- Provide exact file paths and code-level guidance.
- Avoid vague recommendations.

## Specific focus areas
[User's specific concerns: security, performance, correctness, architecture, etc.]

---
```

## Workflow Example

User: "Package the auth folder for another AI to review for security issues"

1. Run the export:
   ```bash
   ~/.claude/skills/codebase-export/scripts/export.sh /path/to/auth
   ```

2. Prepend context block using Edit tool (fill in the template above with specifics about the auth module)

3. Open the download link:
   ```bash
   ~/bin/open https://www.tonki.dev/downloads/auth_export.md
   ```

4. Tell the user the download URL

## Ignored Patterns

**Folders (exact match):** `node_modules`, `dist`, `build`, `__pycache__`, `.git`, `.vscode`, `.idea`, `.next`, `coverage`, `.cache`, `vendor`

**Folders (prefix match):** `.` (hidden), `@` (like @types)

**Sensitive files:** `.env*`, `id_rsa`, `id_ed25519`, `credentials*`, `secrets.*`, `*.pem`, `*.key`, `*.p12`

**Binary extensions:** images, videos, archives, executables, fonts, compiled files, lock files

## Tips

- For large codebases, prefer file-list mode to export only relevant files
- Use glob patterns like `src/**/*.ts` for selective exports (remember: unquoted!)
- The structure section lists relative paths for easy navigation
- Check the summary output for excluded file counts

``````

