No APIs.
No installs.

Just one file.

Looking for a different version? Click here

ca.sh (eslint + tsc)
#!/usr/bin/env sh
set -u

# Find and define commands
find_cmd() {
  if [ -x "./node_modules/.bin/$1" ]; then
    printf '%s\n' "./node_modules/.bin/$1"
    return 0
  elif command -v "$1" >/dev/null 2>&1; then
    command -v "$1"
    return 0
  fi
  return 1
}

ESLINT_CMD=$(find_cmd eslint) || { echo "eslint not found."; exit 1; }
TSC_CMD=$(find_cmd tsc) || { echo "tsc not found."; exit 1; }
NODE_CMD=$(command -v node) || { echo "node not found."; exit 1; }

# Create temporary files with fallback if mktemp missing
mkf() {
  mktemp 2>/dev/null || {
    f="/tmp/portable.$$.$1"
    (umask 077; : >"$f")
    printf '%s\n' "$f"
  }
}
ESLINT_JSON=$(mkf eslint.json)
TSC_OUT=$(mkf tsc.out)
OUT_TXT=$(mkf out.txt)
FILES_LIST=$(mkf files.list)

# Write ESLint JSON to temporary file
"$ESLINT_CMD" . --no-color -f json >"$ESLINT_JSON" 2>"$ESLINT_JSON.err" || true

# Write Typescript Compiler output to temporary file
"$TSC_CMD" --noEmit --incremental false --pretty false >"$TSC_OUT" 2>&1 || true

# Use Node to:
#   - parse ESLint JSON for diagnostics and paths
#   - parse Typescript Compiler output for paths
#   - resolve absolute paths
#   - dedupe duplicate paths
#   - write diagnositics
#   - write file paths
#   - write prompt if diagnostics are found
"$NODE_CMD" - "$PWD" "$ESLINT_JSON" "$TSC_OUT" "$OUT_TXT" "$FILES_LIST" <<'NODE'
// Define constants
const fs = require('fs');
const path = require('path');
const [pwd, eslintJsonPath, tscOutPath, outTxtPath, filesListPath] = process.argv.slice(2);
const outLines = [];
const files = new Set();

// Add normalized absolute file path to files set
function appendFile(filePath) {
  if (!filePath) return;

  try {
    const resolvedPath = path.resolve(pwd, filePath);
    let absolutePath;

    try {
      absolutePath = fs.realpathSync.native(resolvedPath);
    } catch {
      absolutePath = resolvedPath;
    }

    files.add(absolutePath);
  } catch {}
}

// Parse ESLint JSON output
let eslintOut = false;
try {
  const raw = fs.readFileSync(eslintJsonPath, 'utf8');
  const arr = JSON.parse(raw);
  for (const entry of arr) {
    if (entry && Array.isArray(entry.messages) && entry.messages.length) {
      eslintOut = true;
      const rel = path.relative(pwd, entry.filePath || '');
      for (const m of entry.messages) {
        const rule = m.ruleId ? ` [${m.ruleId}]` : '';
        outLines.push(`${rel}:${m.line || 0}:${m.column || 0}: ${m.severity === 2 ? 'error' : 'warn'}: ${m.message}${rule}`);
      }
      appendFile(entry.filePath);
    }
  }
} catch {}

// Parse TypeScript Compiler output
let tscOut = false;
try {
  const txt = fs.readFileSync(tscOutPath, 'utf8');

  const reParen = /^(.+?)\((\d+),(\d+)\):\s*error TS\d+:\s*(.+)$/gm;
  const reColon = /^(.+?):(\d+):(\d+)\s*-\s*error TS\d+:\s*(.+)$/gm;

  function pushTs(re) {
    let m;
    while ((m = re.exec(txt)) !== null) {
      tscOut = true;
      const absPath = m[1];
      const line = m[2];
      const col  = m[3];
      const msg  = m[4];
      const rel = path.relative(pwd, absPath);
      outLines.push(`${rel}:${line}:${col}: error: ${msg}`);
      appendFile(absPath);
    }
  }

  pushTs(reParen);
  pushTs(reColon);
} catch {}

// Write prompt and file paths
if (outLines.length > 0) {
  outLines.unshift('');
  outLines.unshift('The following are the diagnostics from the TypeScript compiler and ESLint. Tell me how to fix every error and/or warning. Tell me exactly what lines I need to modify in what file and what I need to change them to.');
  fs.writeFileSync(outTxtPath, outLines.join('\n') + '\n', 'utf8');
  fs.writeFileSync(filesListPath, Array.from(files).join('\n') + '\n', 'utf8');
} else {
  try { fs.writeFileSync(outTxtPath, ''); } catch {}
  try { fs.writeFileSync(filesListPath, ''); } catch {}
}
NODE

# Append files with line numbers if any diagnostics.
if [ -s "$OUT_TXT" ]; then
  while IFS= read -r path; do
    [ -f "$path" ] || continue
    printf '\n## %s\n' "$path" >> "$OUT_TXT"
    nl -b a "$path" >> "$OUT_TXT"
  done < "$FILES_LIST"
fi

# Delete temporary files
REMOVE_OUT_TXT=0
delete_tmp() {
  rm -f -- "$ESLINT_JSON" "$TSC_OUT" "$FILES_LIST"
  [ "${REMOVE_OUT_TXT:-0}" -eq 1 ] && rm -f -- "$OUT_TXT"
}
trap delete_tmp 0 INT TERM HUP

# Attempt to copy to system clipboard
copy_clipboard() {
  if command -v pbcopy >/dev/null 2>&1; then pbcopy; return 0; fi
  if command -v wl-copy >/dev/null 2>&1; then wl-copy; return 0; fi
  if command -v xclip   >/dev/null 2>&1; then xclip -selection clipboard; return 0; fi
  if command -v xsel    >/dev/null 2>&1; then xsel --clipboard --input; return 0; fi
  if command -v clip.exe>/dev/null 2>&1; then clip.exe; return 0; fi
  return 1
}

# Print summary
if [ -s "$OUT_TXT" ]; then
    if copy_clipboard < "$OUT_TXT"; then
        echo "Copied context to clipboard."
        REMOVE_OUT_TXT=1
    else
        echo "Diagnostics found. Clipboard tool not available. See: $OUT_TXT"
        REMOVE_OUT_TXT=0
    fi
else
    echo "No warnings or errors found. Nothing was copied to clipboard."
    REMOVE_OUT_TXT=1
fi

Features

  • Free

    No rate limits, no API keys. Switch to any provider, at any time.

  • Easy Integration

    No package managers, no dependencies*. Just one copy and paste or download.

  • Customizable

    Fine-tune your prompt, exactly how you like it.

  • Choose Any Provider

    Paste your context directly into any chatbot UI.

  • No Project Modifications

    No permanent files will be created, modified, or deleted.

  • Open Source

    Add features. Open issues. All the code is on Github.

*Node.js, ESLint, and TypeScript are required, but we assume you already have them if you're using this.

How It Works

Setup Shell Script

Set up the shell environment and allow errors.

Lines 1–2

Define Commands

Find and define eslint, tsc, and node commands.

Lines 4–18

Create Temporary Files

Create temporary files, and fallback if mktemp is missing.

Lines 20–31

Run ESLint

Run ESLint and write JSON output to a temporary file.

Lines 33–34

Run TypeScript Compiler

Run Typescript Compiler write output to a temporary file.

Lines 36–37

Use Node

Use Node for JavaScript.

Lines 39–47

Define constants

Import filesystem, path, and temporary file paths. Create variables.

Lines 48–53

Add File Paths

Append normalized absolute file paths to a temporary file.

Lines 55–71

Parse ESLint

Parse ESLint JSON output from temporary file.

Lines 73–89

Parse TypeScript Compiler

Parse TypeScript Compiler output from temporary file.

Lines 91–115

Write Header

Write and format the prompt with diagnostics from both ESLint and TypeScript Compiler.

Lines 117–126

Append Files

Add the contents of the files mentioned in the diagnostics with line numbers.

Lines 129–136

Delete Temporary Files

Delete temporary files, keeping OUT_TXT if unable to copy to clipboard.

Lines 138–144

Copy to Clipboard

Attempt to copy to the system clipboard. Returns 1 if unsuccessful.

Lines 146–154

Print summary

Print success or failure and what action was done.

Lines 156–168

ca.sh
#!/usr/bin/env sh
set -u

# Find and define commands
find_cmd() {
  if [ -x "./node_modules/.bin/$1" ]; then
    printf '%s\n' "./node_modules/.bin/$1"
    return 0
  elif command -v "$1" >/dev/null 2>&1; then
    command -v "$1"
    return 0
  fi
  return 1
}

ESLINT_CMD=$(find_cmd eslint) || { echo "eslint not found."; exit 1; }
TSC_CMD=$(find_cmd tsc) || { echo "tsc not found."; exit 1; }
NODE_CMD=$(command -v node) || { echo "node not found."; exit 1; }

# Create temporary files with fallback if mktemp missing
mkf() {
  mktemp 2>/dev/null || {
    f="/tmp/portable.$$.$1"
    (umask 077; : >"$f")
    printf '%s\n' "$f"
  }
}
ESLINT_JSON=$(mkf eslint.json)
TSC_OUT=$(mkf tsc.out)
OUT_TXT=$(mkf out.txt)
FILES_LIST=$(mkf files.list)

# Write ESLint JSON to temporary file
"$ESLINT_CMD" . --no-color -f json >"$ESLINT_JSON" 2>"$ESLINT_JSON.err" || true

# Write Typescript Compiler output to temporary file
"$TSC_CMD" --noEmit --incremental false --pretty false >"$TSC_OUT" 2>&1 || true

# Use Node to:
#   - parse ESLint JSON for diagnostics and paths
#   - parse Typescript Compiler output for paths
#   - resolve absolute paths
#   - dedupe duplicate paths
#   - write diagnositics
#   - write file paths
#   - write prompt if diagnostics are found
"$NODE_CMD" - "$PWD" "$ESLINT_JSON" "$TSC_OUT" "$OUT_TXT" "$FILES_LIST" <<'NODE'
// Define constants
const fs = require('fs');
const path = require('path');
const [pwd, eslintJsonPath, tscOutPath, outTxtPath, filesListPath] = process.argv.slice(2);
const outLines = [];
const files = new Set();

// Add normalized absolute file path to files set
function appendFile(filePath) {
  if (!filePath) return;

  try {
    const resolvedPath = path.resolve(pwd, filePath);
    let absolutePath;

    try {
      absolutePath = fs.realpathSync.native(resolvedPath);
    } catch {
      absolutePath = resolvedPath;
    }

    files.add(absolutePath);
  } catch {}
}

// Parse ESLint JSON output
let eslintOut = false;
try {
  const raw = fs.readFileSync(eslintJsonPath, 'utf8');
  const arr = JSON.parse(raw);
  for (const entry of arr) {
    if (entry && Array.isArray(entry.messages) && entry.messages.length) {
      eslintOut = true;
      const rel = path.relative(pwd, entry.filePath || '');
      for (const m of entry.messages) {
        const rule = m.ruleId ? ` [${m.ruleId}]` : '';
        outLines.push(`${rel}:${m.line || 0}:${m.column || 0}: ${m.severity === 2 ? 'error' : 'warn'}: ${m.message}${rule}`);
      }
      appendFile(entry.filePath);
    }
  }
} catch {}

// Parse TypeScript Compiler output
let tscOut = false;
try {
  const txt = fs.readFileSync(tscOutPath, 'utf8');

  const reParen = /^(.+?)\((\d+),(\d+)\):\s*error TS\d+:\s*(.+)$/gm;
  const reColon = /^(.+?):(\d+):(\d+)\s*-\s*error TS\d+:\s*(.+)$/gm;

  function pushTs(re) {
    let m;
    while ((m = re.exec(txt)) !== null) {
      tscOut = true;
      const absPath = m[1];
      const line = m[2];
      const col  = m[3];
      const msg  = m[4];
      const rel = path.relative(pwd, absPath);
      outLines.push(`${rel}:${line}:${col}: error: ${msg}`);
      appendFile(absPath);
    }
  }

  pushTs(reParen);
  pushTs(reColon);
} catch {}

// Write prompt and file paths
if (outLines.length > 0) {
  outLines.unshift('');
  outLines.unshift('The following are the diagnostics from the TypeScript compiler and ESLint. Tell me how to fix every error and/or warning. Tell me exactly what lines I need to modify in what file and what I need to change them to.');
  fs.writeFileSync(outTxtPath, outLines.join('\n') + '\n', 'utf8');
  fs.writeFileSync(filesListPath, Array.from(files).join('\n') + '\n', 'utf8');
} else {
  try { fs.writeFileSync(outTxtPath, ''); } catch {}
  try { fs.writeFileSync(filesListPath, ''); } catch {}
}
NODE

# Append files with line numbers if any diagnostics.
if [ -s "$OUT_TXT" ]; then
  while IFS= read -r path; do
    [ -f "$path" ] || continue
    printf '\n## %s\n' "$path" >> "$OUT_TXT"
    nl -b a "$path" >> "$OUT_TXT"
  done < "$FILES_LIST"
fi

# Delete temporary files
REMOVE_OUT_TXT=0
delete_tmp() {
  rm -f -- "$ESLINT_JSON" "$TSC_OUT" "$FILES_LIST"
  [ "${REMOVE_OUT_TXT:-0}" -eq 1 ] && rm -f -- "$OUT_TXT"
}
trap delete_tmp 0 INT TERM HUP

# Attempt to copy to system clipboard
copy_clipboard() {
  if command -v pbcopy >/dev/null 2>&1; then pbcopy; return 0; fi
  if command -v wl-copy >/dev/null 2>&1; then wl-copy; return 0; fi
  if command -v xclip   >/dev/null 2>&1; then xclip -selection clipboard; return 0; fi
  if command -v xsel    >/dev/null 2>&1; then xsel --clipboard --input; return 0; fi
  if command -v clip.exe>/dev/null 2>&1; then clip.exe; return 0; fi
  return 1
}

# Print summary
if [ -s "$OUT_TXT" ]; then
    if copy_clipboard < "$OUT_TXT"; then
        echo "Copied context to clipboard."
        REMOVE_OUT_TXT=1
    else
        echo "Diagnostics found. Clipboard tool not available. See: $OUT_TXT"
        REMOVE_OUT_TXT=0
    fi
else
    echo "No warnings or errors found. Nothing was copied to clipboard."
    REMOVE_OUT_TXT=1
fi

Customize

OS Interface

ca.sh
#!/usr/bin/env sh
set -u

# Find and define commands
find_cmd() {
  if [ -x "./node_modules/.bin/$1" ]; then
    printf '%s\n' "./node_modules/.bin/$1"
    return 0
  elif command -v "$1" >/dev/null 2>&1; then
    command -v "$1"
    return 0
  fi
  return 1
}

ESLINT_CMD=$(find_cmd eslint) || { echo "eslint not found."; exit 1; }
TSC_CMD=$(find_cmd tsc) || { echo "tsc not found."; exit 1; }
NODE_CMD=$(command -v node) || { echo "node not found."; exit 1; }

# Create temporary files with fallback if mktemp missing
mkf() {
  mktemp 2>/dev/null || {
    f="/tmp/portable.$$.$1"
    (umask 077; : >"$f")
    printf '%s\n' "$f"
  }
}
ESLINT_JSON=$(mkf eslint.json)
TSC_OUT=$(mkf tsc.out)
OUT_TXT=$(mkf out.txt)
FILES_LIST=$(mkf files.list)

# Write ESLint JSON to temporary file
"$ESLINT_CMD" . --no-color -f json >"$ESLINT_JSON" 2>"$ESLINT_JSON.err" || true

# Write Typescript Compiler output to temporary file
"$TSC_CMD" --noEmit --incremental false --pretty false >"$TSC_OUT" 2>&1 || true

# Use Node to:
#   - parse ESLint JSON for diagnostics and paths
#   - parse Typescript Compiler output for paths
#   - resolve absolute paths
#   - dedupe duplicate paths
#   - write diagnositics
#   - write file paths
#   - write prompt if diagnostics are found
"$NODE_CMD" - "$PWD" "$ESLINT_JSON" "$TSC_OUT" "$OUT_TXT" "$FILES_LIST" <<'NODE'
// Define constants
const fs = require('fs');
const path = require('path');
const [pwd, eslintJsonPath, tscOutPath, outTxtPath, filesListPath] = process.argv.slice(2);
const outLines = [];
const files = new Set();

// Add normalized absolute file path to files set
function appendFile(filePath) {
  if (!filePath) return;

  try {
    const resolvedPath = path.resolve(pwd, filePath);
    let absolutePath;

    try {
      absolutePath = fs.realpathSync.native(resolvedPath);
    } catch {
      absolutePath = resolvedPath;
    }

    files.add(absolutePath);
  } catch {}
}

// Parse ESLint JSON output
let eslintOut = false;
try {
  const raw = fs.readFileSync(eslintJsonPath, 'utf8');
  const arr = JSON.parse(raw);
  for (const entry of arr) {
    if (entry && Array.isArray(entry.messages) && entry.messages.length) {
      eslintOut = true;
      const rel = path.relative(pwd, entry.filePath || '');
      for (const m of entry.messages) {
        const rule = m.ruleId ? ` [${m.ruleId}]` : '';
        outLines.push(`${rel}:${m.line || 0}:${m.column || 0}: ${m.severity === 2 ? 'error' : 'warn'}: ${m.message}${rule}`);
      }
      appendFile(entry.filePath);
    }
  }
} catch {}

// Parse TypeScript Compiler output
let tscOut = false;
try {
  const txt = fs.readFileSync(tscOutPath, 'utf8');

  const reParen = /^(.+?)\((\d+),(\d+)\):\s*error TS\d+:\s*(.+)$/gm;
  const reColon = /^(.+?):(\d+):(\d+)\s*-\s*error TS\d+:\s*(.+)$/gm;

  function pushTs(re) {
    let m;
    while ((m = re.exec(txt)) !== null) {
      tscOut = true;
      const absPath = m[1];
      const line = m[2];
      const col  = m[3];
      const msg  = m[4];
      const rel = path.relative(pwd, absPath);
      outLines.push(`${rel}:${line}:${col}: error: ${msg}`);
      appendFile(absPath);
    }
  }

  pushTs(reParen);
  pushTs(reColon);
} catch {}

// Write prompt and file paths
if (outLines.length > 0) {
  outLines.unshift('');
  outLines.unshift('The following are the diagnostics from the TypeScript compiler and ESLint. Tell me how to fix every error and/or warning. Tell me exactly what lines I need to modify in what file and what I need to change them to.');
  fs.writeFileSync(outTxtPath, outLines.join('\n') + '\n', 'utf8');
  fs.writeFileSync(filesListPath, Array.from(files).join('\n') + '\n', 'utf8');
} else {
  try { fs.writeFileSync(outTxtPath, ''); } catch {}
  try { fs.writeFileSync(filesListPath, ''); } catch {}
}
NODE

# Append files with line numbers if any diagnostics.
if [ -s "$OUT_TXT" ]; then
  while IFS= read -r path; do
    [ -f "$path" ] || continue
    printf '\n## %s\n' "$path" >> "$OUT_TXT"
    nl -b a "$path" >> "$OUT_TXT"
  done < "$FILES_LIST"
fi

# Delete temporary files
REMOVE_OUT_TXT=0
delete_tmp() {
  rm -f -- "$ESLINT_JSON" "$TSC_OUT" "$FILES_LIST"
  [ "${REMOVE_OUT_TXT:-0}" -eq 1 ] && rm -f -- "$OUT_TXT"
}
trap delete_tmp 0 INT TERM HUP

# Attempt to copy to system clipboard
copy_clipboard() {
  if command -v pbcopy >/dev/null 2>&1; then pbcopy; return 0; fi
  if command -v wl-copy >/dev/null 2>&1; then wl-copy; return 0; fi
  if command -v xclip   >/dev/null 2>&1; then xclip -selection clipboard; return 0; fi
  if command -v xsel    >/dev/null 2>&1; then xsel --clipboard --input; return 0; fi
  if command -v clip.exe>/dev/null 2>&1; then clip.exe; return 0; fi
  return 1
}

# Print summary
if [ -s "$OUT_TXT" ]; then
    if copy_clipboard < "$OUT_TXT"; then
        echo "Copied context to clipboard."
        REMOVE_OUT_TXT=1
    else
        echo "Diagnostics found. Clipboard tool not available. See: $OUT_TXT"
        REMOVE_OUT_TXT=0
    fi
else
    echo "No warnings or errors found. Nothing was copied to clipboard."
    REMOVE_OUT_TXT=1
fi

Contribute

Feel free to contribute to this project. It's open source on GitHub under the MIT License. Don't expect the code the be clean; it's just a weekend project to test out static websites hosted on Cloudflare. Found a bug? Report it here.

2025 Ryan Hsieh