commit c8f85a12202673b39469e8cc96cd28cfb7296229 Author: kaitranntt Date: Sat Nov 1 17:01:42 2025 -0400 feat: initial commit - ultra-simple claude code switcher - Core ccs bash script with profile switching - Installation script with PATH verification - Example config template - Comprehensive README with troubleshooting - Security: input validation, injection prevention - MIT license diff --git a/.ccs.example.json b/.ccs.example.json new file mode 100644 index 00000000..547e7f40 --- /dev/null +++ b/.ccs.example.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "glm": "~/.claude/glm.settings.json", + "sonnet": "~/.claude/sonnet.settings.json", + "opus": "~/.claude/opus.settings.json", + "default": "~/.claude/settings.json" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f73c5fc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# User config files +.ccs.json + +# macOS +.DS_Store + +# Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..13db7a70 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 CCS Contributors + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..6813af39 --- /dev/null +++ b/README.md @@ -0,0 +1,204 @@ +# CCS - Claude Code Switch + +Ultra-simple wrapper for `claude --settings`. Switch between Claude profiles with friendly aliases. + +## Why? + +Claude CLI supports `--settings` flag but it's verbose: + +```bash +claude --settings ~/.claude/glm.settings.json +claude --settings ~/.claude/sonnet.settings.json --verbose +``` + +CCS makes it friendly: + +```bash +ccs glm +ccs sonnet --verbose +``` + +## Installation + +```bash +git clone https://github.com/yourusername/ccs.git +cd ccs +./install.sh +``` + +Or as a one-liner: + +```bash +git clone https://github.com/yourusername/ccs.git && cd ccs && ./install.sh +``` + +## Configuration + +Copy example config: + +```bash +cp .ccs.example.json ~/.ccs.json +``` + +Edit `~/.ccs.json` with your profiles: + +```json +{ + "profiles": { + "glm": "~/.claude/glm.settings.json", + "sonnet": "~/.claude/sonnet.settings.json" + } +} +``` + +## Usage + +### Basic + +```bash +ccs glm # Use GLM profile +ccs sonnet # Use Sonnet profile +``` + +### Pass Arguments Transparently + +All arguments after the profile name are passed directly to Claude: + +```bash +ccs glm --verbose +ccs sonnet /plan "add feature" +ccs opus --model claude-opus-4 +``` + +## Requirements + +- `bash` (3.2+ compatible) +- `jq` (JSON processor) +- [Claude CLI](https://docs.claude.com/en/docs/claude-code/installation) installed + +### Installing jq + +```bash +# macOS +brew install jq + +# Ubuntu/Debian +sudo apt install jq + +# Fedora +sudo dnf install jq +``` + +## How It Works + +1. Reads profile name from first argument +2. Looks up settings file path in `~/.ccs.json` +3. Executes `claude --settings [remaining-args]` + +That's it. No magic, no proxies, no file modification. + +## Troubleshooting + +### Profile not found + +```bash +Error: Profile 'foo' not found in ~/.ccs.json +``` + +**Solution**: Add the profile to your `~/.ccs.json` config file. + +### Settings file missing + +```bash +Error: Settings file not found: ~/.claude/foo.settings.json +``` + +**Solution**: Create the settings file or update the path in your config. + +### jq not installed + +```bash +Error: jq is required but not installed +``` + +**Solution**: Install jq using your package manager (see Requirements section). + +### PATH not set + +```bash +⚠️ Warning: ~/.local/bin is not in PATH +``` + +**Solution**: Add to your shell profile (`~/.bashrc` or `~/.zshrc`): + +```bash +export PATH="$HOME/.local/bin:$PATH" +``` + +Then restart your shell or run `source ~/.bashrc` (or `~/.zshrc`). + +## Use Cases + +### Claude Subscription + GLM Coding Plan + +If you have both Claude subscription and GLM coding plan: + +```json +{ + "profiles": { + "claude": "~/.claude/sonnet.settings.json", + "glm": "~/.claude/glm.settings.json" + } +} +``` + +```bash +ccs claude # Use Claude subscription +ccs glm # Use GLM coding plan +``` + +### Multiple Anthropic Accounts + +```json +{ + "profiles": { + "work": "~/.claude/work.settings.json", + "personal": "~/.claude/personal.settings.json" + } +} +``` + +### Different Models + +```json +{ + "profiles": { + "sonnet": "~/.claude/sonnet.settings.json", + "opus": "~/.claude/opus.settings.json", + "haiku": "~/.claude/haiku.settings.json" + } +} +``` + +## Uninstallation + +```bash +rm ~/.local/bin/ccs +rm ~/.ccs.json +``` + +## License + +MIT + +## Contributing + +PRs welcome! Keep it simple (KISS principle). + +## Philosophy + +**YAGNI**: We don't add features "just in case" +**KISS**: Simple bash script, no complexity +**DRY**: One source of truth (config file) + +This tool does ONE thing well: map profile names to settings files. Nothing more. diff --git a/ccs b/ccs new file mode 100755 index 00000000..d2f0e68e --- /dev/null +++ b/ccs @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs.json}" +PROFILE="${1:-}" + +# Validate usage +if [[ -z "$PROFILE" ]]; then + echo "Usage: ccs [claude-args...]" + echo "Example: ccs glm --verbose" + echo "" + echo "Available profiles are defined in: $CONFIG_FILE" + exit 1 +fi + +# Check config exists +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Error: Config file not found: $CONFIG_FILE" + echo "" + echo "Create ~/.ccs.json with your profile mappings." + echo "See .ccs.example.json for template." + exit 1 +fi + +# Check jq installed +if ! command -v jq &> /dev/null; then + echo "Error: jq is required but not installed" + echo "" + echo "Install jq:" + echo " macOS: brew install jq" + echo " Ubuntu: sudo apt install jq" + echo " Fedora: sudo dnf install jq" + exit 1 +fi + +# Validate profile name (alphanumeric, dash, underscore only) +if [[ "$PROFILE" =~ [^a-zA-Z0-9_-] ]]; then + echo "Error: Invalid profile name. Use only alphanumeric characters, dash, or underscore." + exit 1 +fi + +# Validate JSON syntax +if ! jq -e . "$CONFIG_FILE" &>/dev/null; then + echo "Error: Invalid JSON in $CONFIG_FILE" + exit 1 +fi + +# Validate config has profiles object +if ! jq -e '.profiles' "$CONFIG_FILE" &>/dev/null; then + echo "Error: Config must have 'profiles' object" + echo "See .ccs.example.json for correct format" + exit 1 +fi + +# Get settings path for profile (using --arg to prevent injection) +SETTINGS_PATH=$(jq -r --arg profile "$PROFILE" '.profiles[$profile] // empty' "$CONFIG_FILE") + +if [[ -z "$SETTINGS_PATH" ]]; then + echo "Error: Profile '$PROFILE' not found in $CONFIG_FILE" + echo "" + echo "Available profiles:" + jq -r '.profiles | keys[]' "$CONFIG_FILE" 2>/dev/null | sed 's/^/ - /' + exit 1 +fi + +# Expand ~ in path +SETTINGS_PATH="${SETTINGS_PATH/#\~/$HOME}" + +# Validate settings file exists +if [[ ! -f "$SETTINGS_PATH" ]]; then + echo "Error: Settings file not found: $SETTINGS_PATH" + echo "" + echo "Create the settings file or update the path in $CONFIG_FILE" + exit 1 +fi + +# Shift profile arg, pass rest to claude +shift + +# Execute claude with settings +exec claude --settings "$SETTINGS_PATH" "$@" diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..a44a8bc5 --- /dev/null +++ b/install.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "Installing ccs to $INSTALL_DIR..." +echo "" + +# Create install dir if needed +mkdir -p "$INSTALL_DIR" + +# Make executable first +chmod +x "$SCRIPT_DIR/ccs" + +# Symlink ccs script +ln -sf "$SCRIPT_DIR/ccs" "$INSTALL_DIR/ccs" + +# Verify installation +if [[ ! -L "$INSTALL_DIR/ccs" ]]; then + echo "❌ Error: Failed to create symlink at $INSTALL_DIR/ccs" + echo "Check directory permissions and try again." + exit 1 +fi + +# Check if in PATH +if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then + echo "⚠️ Warning: $INSTALL_DIR is not in PATH" + echo "" + echo "Add to your shell profile (~/.bashrc or ~/.zshrc):" + echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" + echo "" +fi + +echo "✅ Installation complete!" +echo "" +echo "Next steps:" +echo "1. Copy example config: cp $SCRIPT_DIR/.ccs.example.json ~/.ccs.json" +echo "2. Edit ~/.ccs.json with your profile mappings" +echo "3. Run: ccs " +echo "" +echo "Example:" +echo " ccs glm" +echo " ccs sonnet --verbose"