fix(core): add VERSION file handling and comprehensive test coverage

This commit fixes version management and argument parsing issues across
both Windows and Linux/macOS platforms, achieving 100% test coverage.

Changes:
- Add VERSION file installation to both installers (install.sh, install.ps1)
- Fix version/help command detection when using 'powershell -File' syntax
- Rename PowerShell param from $Profile to $ProfileOrFlag for clarity
- Add $FirstArg detection to check both ProfileOrFlag and RemainingArgs
- Create comprehensive edge case test suites for both platforms:
  * tests/edge-cases.ps1 - 31 tests for Windows (100% pass)
  * tests/edge-cases.sh - 37 tests for Linux/macOS (100% pass)
- Fix multiline regex matching in tests for error messages
- Update test expectations to match platform-specific behavior
- Reorganize project structure:
  * Move installers to installers/ directory
  * Add scripts/ directory for version management
  * Add config/ directory for configuration templates
  * Add docs/ directory for documentation

Test Results:
- Windows (PowerShell): 31/31 tests passing (100%)
- Linux/macOS (Bash): 37/37 tests passing (100%)

Breaking Changes: None
- Installers moved but GitHub URLs updated in README files
- All existing functionality preserved

Co-authored-by: Claude Code <claude@anthropic.com>
This commit is contained in:
kaitranntt
2025-11-02 20:27:45 -05:00
co-authored by Claude Code
parent 7bd2e9e4ba
commit e9eb215d1f
24 changed files with 2392 additions and 484 deletions
+197
View File
@@ -0,0 +1,197 @@
---
allowed-tools: Glob, Read, Bash(jq:*), Task
description: Delegate commands to alternative models (GLM, Haiku) for token optimization
argument-hint: [profile] /command [args...]
model: haiku
---
# /ccs - Delegate to Alternative Models
You are a **delegation orchestrator**. Your job is to delegate the user's command to an alternative AI model (GLM, Haiku, etc.) using the CCS (Claude Code Switch) system.
## User's Input
The user invoked: `/ccs {{args}}`
## Your Task
Follow these steps to delegate the command:
### Step 1: Parse Arguments
Extract from the user's input:
- **Profile**: Model profile to use (e.g., `glm`, `haiku`, `son`) - defaults to `glm` if omitted
- **Command**: The slash command to delegate (e.g., `plan`, `code`, `debug`)
- **Arguments**: Additional arguments to pass to the command
**Examples**:
- `/ccs glm /plan "add auth"` → profile=`glm`, command=`plan`, args=`"add auth"`
- `/ccs /code "fix bug"` → profile=`glm` (default), command=`code`, args=`"fix bug"`
- `/ccs haiku /ask "what is X?"` → profile=`haiku`, command=`ask`, args=`"what is X?"`
### Step 2: Validate Profile
Check if the profile exists in `~/.ccs/config.json`:
```bash
jq -e '.profiles["<profile>"]' ~/.ccs/config.json
```
If the profile doesn't exist:
```
❌ Error: Profile '<profile>' not found in ~/.ccs/config.json
Available profiles:
<list profiles from config>
Usage: /ccs [profile] /command [args]
Example: /ccs glm /plan "add authentication"
```
### Step 3: Resolve Command Path
Find the command file in this priority order:
1. **User-scope**: `~/.ccs/commands/<command>.md`
2. **Project-scope**: `.claude/commands/<command>.md`
Use Glob tool to check both locations. If not found in either:
```
❌ Error: Command '/<command>' not found
Searched locations:
- ~/.ccs/commands/<command>.md (user-scope)
- .claude/commands/<command>.md (project-scope)
Check command name spelling or create the command file.
```
### Step 4: Launch Subagent with Task Tool
Use the Task tool to delegate execution:
**Parameters**:
```typescript
{
subagent_type: "general-purpose",
model: "haiku",
description: "Delegating /<command> to <profile> profile",
prompt: `You are executing a delegated command via CCS (Claude Code Switch).
**Delegation Context**:
- Profile: <profile>
- Command: /<command>
- Arguments: <args>
- Command file: <resolved-path>
**CRITICAL**: Before executing, switch to the CCS profile:
\`\`\`bash
ccs <profile>
\`\`\`
**Instructions**:
1. Run \`ccs <profile>\` to switch to the correct model
2. Read the command file at <resolved-path>
3. Execute the command with arguments: <args>
4. Provide a clear summary of what was accomplished
Execute now.`
}
```
### Step 5: Format and Return Result
Present the subagent's output in this format:
```markdown
🤖 **CCS Delegation Result**
**Profile**: <profile>
**Command**: /<command>
---
<subagent-output>
---
💡 *Token optimization: This task was delegated to the '<profile>' profile to conserve primary model usage.*
```
## When to Use /ccs
### ✅ Good Use Cases
- Simple planning tasks
- Straightforward code implementation
- Documentation writing
- Basic debugging
- Quick questions
### ❌ Don't Delegate
- Complex reasoning/architecture
- Security-critical code
- Deep code review
- Context-dependent tasks
- Already using right model
## Error Handling
If delegation fails, provide helpful error message with:
1. What went wrong
2. Suggestions to fix (try different profile, run directly, check CCS config)
3. How to verify CCS setup (`ccs --version`)
## Configuration Check
If profile validation fails, show how to check config:
```bash
# View available profiles
cat ~/.ccs/config.json
# Test CCS is working
ccs --version
```
## Examples
**Example 1**: Simple planning with GLM
```
User: /ccs glm /plan "add user authentication"
You: Parse → profile=glm, command=plan, args="add user authentication"
Validate → Check glm exists in config ✓
Resolve → Find ~/.ccs/commands/plan.md ✓
Launch → Task tool with ccs glm
Return → Formatted result
```
**Example 2**: Quick question with Haiku
```
User: /ccs haiku /ask "explain JWT tokens"
You: Parse → profile=haiku, command=ask, args="explain JWT tokens"
Validate → Check haiku exists ✓
Resolve → Find command ✓
Launch → Delegate to haiku
Return → Result
```
**Example 3**: Default profile
```
User: /ccs /debug "API 500 error"
You: Parse → profile=glm (default), command=debug, args="API 500 error"
Continue with delegation...
```
## Notes
- This is a **meta-command** that orchestrates other commands
- Each delegation creates an **isolated subagent session**
- The subagent switches to the specified CCS profile automatically
- **Token optimization** is the primary benefit
- User-scope commands (`~/.ccs/commands/`) are checked first
- Project-scope commands (`.claude/commands/`) are fallback
## Related Documentation
- Setup guide: `tools/ccs/SETUP-DELEGATION.md`
- Delegation patterns: `tools/ccs/skills/ccs-delegation.md`
- CCS Tool: `tools/ccs/README.md`
+187
View File
@@ -0,0 +1,187 @@
---
name: ccs-delegation
description: Use this skill when the user invokes the `/ccs` command or requests delegating tasks to alternative models (GLM, Haiku) for token optimization. This skill guides when and how to delegate commands to save primary model tokens.
---
# CCS Delegation
Intelligent task delegation to alternative AI models (GLM, Haiku, etc.) for token optimization using the `/ccs` meta-command.
## Purpose
The `/ccs` command delegates simple tasks to alternative models while staying in the primary session, optimizing:
- **Token efficiency**: Save primary model tokens for complex work
- **Task-model matching**: Use appropriate model for each task
- **Cost optimization**: Route simple tasks to lower-cost models
## When to Invoke This Skill
Load this skill when:
- User explicitly invokes `/ccs [profile] /command [args]`
- User requests delegating tasks to alternative models
- User asks to use GLM/Haiku for a task
- User requests token conservation strategies
## Decision Framework
### ✅ Delegate to Alternative Models
Recommend `/ccs` when:
**Simple, straightforward tasks**:
- Basic planning (CRUD operations, simple features)
- Straightforward code implementation
- Documentation writing
- Simple bug fixes
- Routine refactoring
**Token conservation scenarios**:
- Working on complex project, saving tokens for hard parts
- Rate limit approaching on primary model
- Cost-conscious development
**User explicitly requests**:
- "Use GLM for this"
- "Delegate to cheaper model"
- "Save tokens on this task"
### ❌ Keep in Primary Model
Don't recommend delegation when:
**Complex reasoning required**:
- Architecture decisions
- System design patterns
- Complex debugging (multiple files)
- Security-critical code review
**Context-dependent**:
- Needs current session context
- Requires back-and-forth interaction
- Builds on previous conversation
**Quality-critical**:
- Production-critical code
- Security implementations
- Performance-sensitive algorithms
- Public-facing API design
## Quick Decision Tree
```
Is task simple and straightforward?
↓ NO → Keep in current model
↓ YES
Does task need deep context from current session?
↓ YES → Keep in current model
↓ NO
Is task security or quality critical?
↓ YES → Keep in current model
↓ NO
✅ RECOMMEND /ccs delegation
```
## Usage Patterns
### Pattern 1: Explicit Delegation Request
When user explicitly requests alternative model:
```
User: "Use GLM to plan the authentication feature"
Claude: "I'll delegate this planning task to GLM to conserve tokens."
<Invokes: /ccs glm /plan "design authentication feature">
```
### Pattern 2: Proactive Token Optimization
When task is clearly simple, proactively suggest:
```
User: "/plan add a simple CRUD endpoint for users"
Claude: "This is straightforward. I'll delegate to GLM to save tokens."
<Invokes: /ccs glm /plan "add CRUD endpoint for users">
```
### Pattern 3: Automatic Model Selection
For simple tasks without explicit profile request:
```
User: "/ccs /code 'implement the auth endpoints'"
Claude: "Delegating to GLM (default profile) for implementation."
<Invokes command with default glm profile>
```
## Profile Selection Guide
**GLM (glm profile)**:
- Simple coding tasks
- Basic planning
- Documentation
- Routine fixes
- Default choice for simple tasks
**Haiku (haiku profile)**:
- Quick questions
- Simple explanations
- Fast, lightweight tasks
- When speed matters
**Sonnet (son profile)**:
- Don't delegate—use directly
- Complex reasoning
- Architecture decisions
- Security-critical work
## Command Format
```bash
/ccs [profile] /command [args...]
```
**Examples**:
- `/ccs glm /plan "add user authentication"`
- `/ccs haiku /ask "explain JWT tokens"`
- `/ccs /code "implement feature"` (defaults to glm)
## Error Handling
If `/ccs` invocation fails:
1. Check if CCS is properly configured: `ccs --version`
2. Verify profile exists in `~/.ccs/config.json`
3. Ensure command exists in `~/.ccs/commands/` or `.claude/commands/`
4. Suggest running command directly if delegation problematic
## Configuration Check
Guide user to verify CCS setup:
```bash
# Check CCS version
ccs --version
# View available profiles
cat ~/.ccs/config.json
# List available commands
ls ~/.ccs/commands/
```
## Integration Notes
- `/ccs` is a meta-command that orchestrates other slash commands
- Each delegation creates isolated subagent session
- Subagent automatically switches to specified CCS profile
- User-scope commands (`~/.ccs/commands/`) checked first
- Project-scope commands (`.claude/commands/`) as fallback
## Related Resources
- Command implementation: `~/.ccs/commands/ccs.md`
- Detailed patterns: `references/delegation-patterns.md`
- Setup guide: `tools/ccs/SETUP-DELEGATION.md`
- CCS Tool: `tools/ccs/README.md`
@@ -0,0 +1,295 @@
# CCS Delegation Patterns - Detailed Reference
This reference provides comprehensive patterns and examples for CCS task delegation.
## Advanced Usage Patterns
### Pattern 4: Task Splitting (Complex + Simple)
Split work between models for optimal efficiency:
```
User: "Design and implement payment system"
Claude (in Sonnet):
1. Design architecture (Sonnet handles complex reasoning)
- Security considerations
- Data flow diagrams
- API contracts
2. Delegate implementation to GLM:
/ccs glm /code "implement payment webhook handler based on this design"
3. Review security (back to Sonnet):
Review the implemented code for security vulnerabilities
```
### Pattern 5: Batch Operations
Delegate multiple simple tasks sequentially:
```bash
# Multiple planning tasks
/ccs glm /plan "feature A: user profile"
/ccs glm /plan "feature B: notifications"
/ccs glm /plan "feature C: search functionality"
# Then review all plans together in Sonnet
"Review all three plans for consistency and integration points"
```
### Pattern 6: Iterative Refinement
Use GLM for initial implementation, Sonnet for refinement:
```
1. /ccs glm /code "implement basic CRUD API"
2. (Sonnet reviews): "Add error handling, input validation, rate limiting"
3. /ccs glm /fix "add suggested improvements"
```
## Model Capability Matrix
### GLM 4.6 - Best For:
- ✅ Simple feature planning (CRUD, basic workflows)
- ✅ Straightforward code implementation
- ✅ Documentation and README files
- ✅ Basic bug fixes (clear error messages)
- ✅ Refactoring with clear scope
- ✅ Test writing (unit tests, simple integration tests)
- ✅ Configuration files (package.json, tsconfig, etc.)
- ❌ Complex algorithms
- ❌ Security-critical code
- ❌ Performance optimization
- ❌ Architecture decisions
### Haiku - Best For:
- ✅ Quick questions ("what is X?", "explain Y")
- ✅ Simple explanations
- ✅ Code snippet explanations
- ✅ Quick documentation lookups
- ✅ Simple troubleshooting
- ❌ Code implementation
- ❌ Complex analysis
- ❌ Multi-step tasks
### Sonnet 4.5 - Best For:
- ✅ Complex architecture and design
- ✅ Security-critical code review
- ✅ Performance optimization
- ✅ Complex debugging (multiple files, unclear root cause)
- ✅ API design and contracts
- ✅ Database schema design
- ✅ Integration planning
- ✅ Ambiguous requirement clarification
## Context Preservation Strategies
### When Delegation Makes Sense
**Low context requirements**:
```
# Current session has 50 lines discussing feature X
# User wants to add unrelated feature Y
/ccs glm /plan "add feature Y" # ✅ Good - Y doesn't need X's context
```
**Self-contained tasks**:
```
# Task has all info in the prompt
/ccs glm /code "implement function that validates email addresses" # ✅ Good
```
### When to Keep in Current Session
**High context dependency**:
```
# Current session has extensive discussion about auth flow
# User: "now implement the login function"
# ❌ Don't delegate - needs current context
```
**Iterative refinement**:
```
# Session has 10 messages refining a complex algorithm
# User: "adjust the algorithm to handle edge case X"
# ❌ Don't delegate - needs full conversation history
```
## Token Efficiency Analysis
### Token Savings Example
**Without delegation**:
- Complex architecture discussion: 15,000 tokens (Sonnet)
- Simple implementation: 5,000 tokens (Sonnet)
- Total: 20,000 Sonnet tokens
**With delegation**:
- Complex architecture discussion: 15,000 tokens (Sonnet)
- Delegate implementation: 5,000 tokens (GLM)
- Total: 15,000 Sonnet + 5,000 GLM tokens
- **Savings**: 5,000 Sonnet tokens
### When Delegation Overhead Outweighs Benefits
**Very simple tasks** (< 200 tokens):
```
User: "add a comment to this line"
# ❌ Don't delegate - overhead > benefit
```
**Tasks needing immediate context** (recent discussion):
```
User: "based on what we just discussed, implement X"
# ❌ Don't delegate - needs immediate context
```
## Real-World Workflows
### Workflow 1: New Feature Development
```
1. Requirements gathering (Sonnet)
- Clarify ambiguous requirements
- Discuss trade-offs
- Design API contracts
2. Planning (GLM)
/ccs glm /plan "implement user profile feature based on requirements"
3. Implementation (GLM)
/ccs glm /code "implement the user profile API endpoints"
4. Review (Sonnet)
- Security review
- Performance check
- Integration verification
5. Fixes (GLM if simple, Sonnet if complex)
/ccs glm /fix "address code review comments"
```
### Workflow 2: Bug Investigation & Fix
```
1. Investigation (Sonnet)
- Analyze logs
- Trace root cause
- Understand system state
2. Simple fix (GLM)
/ccs glm /fix "update validation in UserController to handle null emails"
3. Verification (Sonnet)
- Verify fix addresses root cause
- Check for regressions
```
### Workflow 3: Documentation Sprint
```
1. /ccs glm /docs "document the authentication API endpoints"
2. /ccs glm /docs "write setup guide for new developers"
3. /ccs glm /docs "create API usage examples"
4. (Sonnet reviews for completeness and accuracy)
```
## Anti-Patterns to Avoid
### Anti-Pattern 1: Over-Delegation
**Bad**:
```
/ccs glm /plan "design entire microservices architecture"
# Too complex for GLM
```
**Good**:
```
# Sonnet handles architecture
# Then: /ccs glm /code "implement user service based on architecture"
```
### Anti-Pattern 2: Delegation with Hidden Context
**Bad**:
```
# After 20 messages discussing custom auth flow
User: "implement the login"
/ccs glm /code "implement login"
# GLM doesn't have context about custom flow
```
**Good**:
```
# Include context in delegation
/ccs glm /code "implement login using JWT with custom claims: userId, role, tenantId"
```
### Anti-Pattern 3: Micro-Delegation
**Bad**:
```
/ccs glm /code "add variable x"
/ccs glm /code "add function y"
/ccs glm /code "add class z"
# Too much delegation overhead
```
**Good**:
```
/ccs glm /code "implement user management module with CRUD operations"
```
## Troubleshooting
### Issue: Delegated Task Failed
**Symptoms**:
- GLM produces incorrect code
- Implementation doesn't match requirements
- Security vulnerabilities introduced
**Solutions**:
1. Check if task was too complex for GLM
2. Provide more explicit requirements
3. Use Sonnet for complex parts, GLM only for straightforward implementation
4. Review GLM output in Sonnet before accepting
### Issue: Context Loss
**Symptoms**:
- Delegated task doesn't align with previous discussion
- Implementation misses important constraints
- Style doesn't match existing codebase
**Solutions**:
1. Include more context in delegation prompt
2. Don't delegate context-dependent tasks
3. Provide explicit style guidelines in prompt
4. Consider keeping task in current session
### Issue: Frequent Delegation Failures
**Symptoms**:
- Multiple retries needed
- Tasks keep failing validation
- Time spent > time saved
**Solutions**:
1. Re-evaluate task complexity
2. Delegate fewer, larger tasks instead of many small ones
3. Use more specific instructions
4. Consider if delegation is appropriate for this workflow
## Best Practices Summary
1. **Delegate simple, self-contained tasks**
2. **Keep complex reasoning in Sonnet**
3. **Include sufficient context in delegation**
4. **Review delegated output before proceeding**
5. **Monitor token savings vs overhead**
6. **Prefer batch operations over micro-delegations**
7. **Document delegation patterns that work well**
8. **Iterate on delegation strategies based on results**
+28
View File
@@ -0,0 +1,28 @@
name: Deploy CCS CloudFlare Worker
on:
push:
branches: [main]
paths:
- 'scripts/worker.js'
- 'installers/**'
# Allow manual trigger
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy ccs-installer to CloudFlare
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy Worker
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: scripts
command: deploy worker.js --name ccs-installer
+85
View File
@@ -0,0 +1,85 @@
# Changelog
All notable changes to CCS will be documented here.
Format based on [Keep a Changelog](https://keepachangelog.com/).
## [2.1.0] - 2025-11-02
### Changed
- **MAJOR SIMPLIFICATION**: Windows PowerShell now uses `--settings` flag (confirmed working in Claude CLI 2.0.31+)
- Removed 64 lines of environment variable management code from ccs.ps1
- Windows and Unix/Linux/macOS now use identical approach
- Updated all documentation to reflect cross-platform consistency
- ccs.ps1: 235 lines → 171 lines (27% reduction)
### Technical Details
- Windows Claude CLI DOES support `--settings` flag (contrary to previous assumptions)
- No longer manually sets/restores environment variables
- Simpler, cleaner, more maintainable codebase
- Settings file format unchanged (still uses `{"env": {...}}` structure)
## [2.0.0] - 2025-11-02
### BREAKING CHANGES
- Removed `ccs son` profile - use `ccs` (default) for Claude subscription
- Config structure simplified - `sonnet` profile removed from default config
### Added
- `config/` folder with organized templates (base-glm, base-dsp, config.example)
- `config/README.md` - comprehensive config documentation
- `installers/` folder for clean project structure (install/uninstall scripts)
- Smart installer with validation and self-healing
- Non-invasive approach - never modifies `~/.claude/settings.json`
- Version pinning support: `curl ccs.kaitran.ca/v2.0.0/install | bash`
- CHANGELOG.md for release tracking
- WORKFLOW.md - comprehensive workflow documentation
- Migration detection and auto-migration from v1.x configs
- Config backup before modifications with timestamp
- JSON validation for all config files
- GitHub Actions workflow for auto-deploying CloudFlare Worker
- VERSION file for centralized version management
### Fixed
- **CRITICAL**: PowerShell env var bug - strict filtering prevents crashes on non-string values
- PowerShell now requires `env` object in settings files (prevents crashes on root-level fields)
- Type validation for environment variables (strings only)
- Installer now validates all JSON before processing
- Better error messages with actionable solutions
### Changed
- `ccs` now default behavior (uses Claude subscription, no profile needed)
- Simplified profile management (glm fallback only)
- Moved `.ccs.example.json``config/config.example.json`
- Reorganized project: install/uninstall scripts → `installers/` folder
- Enhanced error messages with solutions and reinstall instructions
- Removed sonnet profile creation from installers
- Config structure: `{ "glm": "...", "default": "~/.claude/settings.json" }`
- Worker.js routing updated for new installers/ path
### Migration Guide
- Old users: `ccs son``ccs` (automatic deprecation warning during install)
- Config auto-migrates during installation (son/sonnet profiles removed)
- GLM API keys preserved during upgrade
- Backup created automatically: `~/.ccs/config.json.backup.TIMESTAMP`
- No action needed unless you customized `sonnet` profile
## [1.1.0] - 2025-11-01
### Added
- Support for git worktrees and submodules
- Enhanced GLM profile with default model variables
- Improved installer detection logic
### Fixed
- BASH_SOURCE unbound variable error in installer
- Git worktree detection
## [1.0.0] - 2025-10-31
### Added
- Initial release
- Profile-based switching between Claude and GLM
- Cross-platform support (macOS, Linux, Windows)
- One-line installation
- Auto-detection of current provider
-154
View File
@@ -1,154 +0,0 @@
# CCS Installation Workflow
This diagram illustrates the installation flow for the `ccs` (claude-code-switch) tool, including initialization, installation, profile setup, and completion steps.
## Workflow Diagram
```mermaid
flowchart TD
Start([Start Installation]) --> Init[Initialize Configuration<br/>INSTALL_DIR, CCS_DIR, CLAUDE_DIR]
Init --> DetectMethod{Detect Install Method<br/>ccs file exists?}
DetectMethod -->|Yes: SCRIPT_DIR/ccs| GitMethod[Method: git]
DetectMethod -->|No| StandaloneMethod[Method: standalone]
GitMethod --> CreateDirs[Create Directories<br/>mkdir INSTALL_DIR, CCS_DIR]
StandaloneMethod --> CheckCurl{curl available?}
CheckCurl -->|No| ErrorCurl[❌ Error: curl required]
CheckCurl -->|Yes| CreateDirs
ErrorCurl --> Exit0([Exit 1])
CreateDirs --> InstallExec{Install Method?}
InstallExec -->|Git| InstallGit[Use local ccs from SCRIPT_DIR<br/>chmod +x, ln -sf to INSTALL_DIR]
InstallExec -->|Standalone| DownloadCCS[Download ccs from GitHub<br/>to CCS_DIR<br/>chmod +x, ln -sf to INSTALL_DIR]
InstallGit --> CheckSymlink{Symlink Created?}
DownloadCCS --> CheckDownload{Download success?}
CheckDownload -->|No| ErrorDownload[❌ Error: Failed to download]
CheckDownload -->|Yes| CheckSymlink
ErrorDownload --> Exit5([Exit 1])
CheckSymlink -->|No| ErrorSymlink[❌ Error: Symlink Failed]
CheckSymlink -->|Yes| InstallUninstaller
ErrorSymlink --> Exit1([Exit 1])
InstallUninstaller{uninstall.sh exists?}
InstallUninstaller -->|Local file| CopyUninstall[Copy uninstall.sh]
InstallUninstaller -->|Standalone + curl| FetchUninstall[Fetch from GitHub]
InstallUninstall -->|Neither| SkipUninstall
CopyUninstall --> CheckPath
FetchUninstall --> CheckPath
SkipUninstall --> CheckPath
CheckPath{INSTALL_DIR in PATH?}
CheckPath -->|No| WarnPath[⚠️ Warn: Add to PATH]
CheckPath -->|Yes| DetectProvider
WarnPath --> DetectProvider
DetectProvider[Detect Current Provider<br/>check settings.json] --> ProviderResult{Provider?}
ProviderResult -->|glm| SetGLM[Provider: glm]
ProviderResult -->|claude| SetClaude[Provider: claude]
ProviderResult -->|custom| SetCustom[Provider: custom]
ProviderResult -->|unknown| SetUnknown[Provider: unknown]
SetGLM --> CheckProfiles
SetClaude --> CheckProfiles
SetCustom --> CheckProfiles
SetUnknown --> CheckProfiles
CheckProfiles{Missing Profiles?}
CheckProfiles -->|GLM missing| CreateGLM
CheckProfiles -->|Sonnet missing| CreateSonnet
CheckProfiles -->|Both missing| CreateBoth
CheckProfiles -->|None missing| CreateCCSConfig
CreateGLM{Current Provider = glm?}
CreateGLM -->|Yes| CopyGLMConfig[Copy current config<br/>+ enhance with jq]
CreateGLM -->|No| CreateGLMTemplate[Create GLM template<br/>+ merge with jq if available]
CopyGLMConfig --> CheckJQGLM{jq available?}
CheckJQGLM -->|Yes| EnhanceGLM[Add model settings]
CheckJQGLM -->|No| CopyAsIsGLM[Copy as-is]
EnhanceGLM --> AtomicMVGLM
CopyAsIsGLM --> AtomicMVGLM
CreateGLMTemplate --> CheckJQTemplate{jq available?}
CheckJQTemplate -->|Yes| MergeTemplate[Merge with current]
CheckJQTemplate -->|No| BasicTemplate[Use basic template]
MergeTemplate --> CheckMerge{Merge success?}
CheckMerge -->|Yes| AtomicMVGLM[atomic_mv to ~/.ccs/glm.settings.json]
CheckMerge -->|No| BasicTemplate
BasicTemplate --> WriteGLM[Write ~/.ccs/glm.settings.json]
AtomicMVGLM --> CheckAtomicGLM{Move success?}
CheckAtomicGLM -->|No| ErrorAtomicGLM[❌ Error: Permission denied]
CheckAtomicGLM -->|Yes| WarnAPIKey
WriteGLM --> WarnAPIKey[⚠️ Warn: Replace API key]
ErrorAtomicGLM --> Exit2([Exit 1])
WarnAPIKey --> CreateSonnet
CreateBoth --> CreateGLM
CreateSonnet{Current Provider = claude?}
CreateSonnet -->|Yes| CopySonnetConfig[Copy current config]
CreateSonnet -->|No| CreateSonnetTemplate[Create Sonnet template<br/>+ remove custom settings with jq]
CopySonnetConfig --> WriteSonnet[Write ~/.ccs/sonnet.settings.json]
CreateSonnetTemplate --> CheckJQSonnet{jq available?}
CheckJQSonnet -->|Yes| RemoveCustom[Remove ANTHROPIC_BASE_URL, etc.]
CheckJQSonnet -->|No| BasicSonnet[Use basic template]
RemoveCustom --> CheckRemove{Remove success?}
CheckRemove -->|Yes| AtomicMVSonnet[atomic_mv to ~/.ccs/sonnet.settings.json]
CheckRemove -->|No| BasicSonnet
BasicSonnet --> WriteSonnet
AtomicMVSonnet --> CheckAtomicSonnet{Move success?}
CheckAtomicSonnet -->|No| ErrorAtomicSonnet[❌ Error: Permission denied]
CheckAtomicSonnet -->|Yes| WriteSonnet
ErrorAtomicSonnet --> Exit3([Exit 1])
WriteSonnet --> CreateCCSConfig
CreateCCSConfig{~/.ccs/config.json exists?}
CreateCCSConfig -->|No| WriteCCSConfig[Create ~/.ccs/config.json<br/>with profile mappings]
CreateCCSConfig -->|Yes| SkipCCSConfig[Skip: Already exists]
WriteCCSConfig --> CheckCCSWrite{Write success?}
CheckCCSWrite -->|No| ErrorCCSWrite[❌ Error: Permission denied]
CheckCCSWrite -->|Yes| Complete
ErrorCCSWrite --> Exit4([Exit 1])
SkipCCSConfig --> Complete
Complete[✅ Setup Complete<br/>Display Quick Start Guide] --> End([End])
```
## Key Decision Points
1. **Installation Method**: Detects `ccs` file existence in SCRIPT_DIR (not `.git`)
- Git install: `ccs` file exists → use local file
- Standalone: no `ccs` file → download from GitHub
2. **Provider Detection**: Analyzes `~/.claude/settings.json` to determine current provider (glm, claude, custom, unknown)
3. **Profile Creation**: Creates missing profile files based on current provider
4. **jq Enhancement**: Uses jq for JSON manipulation if available, falls back to basic templates
5. **Atomic Operations**: Uses atomic_mv for safe file operations with permission checks
## Error Handling Paths
- curl not available (standalone install) → Exit 1
- GitHub download failure (standalone install) → Exit 1
- Symlink creation failure → Exit 1
- Atomic file move failures (permissions) → Exit 1
- Missing PATH warning (non-fatal)
- Missing API key warning (non-fatal)
## Profile Templates
- **GLM Profile**: Configured for api.z.ai with glm-4.6 model
- **Sonnet Profile**: Default Claude configuration (no custom base URL)
- **CCS Config**: Maps profile shortcuts (glm, son, default) to settings files
+78 -65
View File
@@ -16,8 +16,8 @@ Manual switching is tedious and error-prone.
**The Solution**:
```bash
ccs son # Complex refactoring? Use Claude Sonnet 4.5
ccs glm # Simple bug fix? Use GLM 4.6
ccs # Use Claude subscription (default)
ccs glm # Switch to GLM fallback
# Hit rate limit? Switch instantly:
ccs glm # Continue working with GLM
```
@@ -38,25 +38,33 @@ curl -fsSL ccs.kaitran.ca/install | bash
irm ccs.kaitran.ca/install.ps1 | iex
```
**Configure**:
**What Gets Installed**:
```bash
# Edit with your profiles
cat > ~/.ccs/config.json << 'EOF'
~/.ccs/
├── ccs # Main executable
├── config.json # Profile configuration
├── glm.settings.json # GLM profile
└── .claude/ # Claude Code integration
├── commands/ccs.md # /ccs meta-command
└── skills/ # Delegation skills
```
**Configure**:
```json
# Installer creates config automatically
# Config: ~/.ccs/config.json
{
"profiles": {
"glm": "~/.ccs/glm.settings.json",
"son": "~/.ccs/sonnet.settings.json",
"default": "~/.claude/settings.json"
}
}
EOF
```
**Use**:
```bash
ccs # Use default profile
ccs glm # Use GLM profile
ccs son # Use Sonnet profile
ccs # Use Claude subscription (default)
ccs glm # Use GLM fallback
# Utility commands
ccs --version # Show CCS version
@@ -85,7 +93,7 @@ ccs --help # Show Claude CLI help
**With CCS**: Switch models based on task complexity, maximize quality while managing costs.
```bash
ccs son # Planning new feature architecture
ccs # Planning new feature architecture
# Got the plan? Implement with GLM:
ccs glm # Write the straightforward code
```
@@ -98,7 +106,7 @@ If you have both Claude subscription and GLM Coding Plan, you know the pain:
- Repeat 10x per day
**CCS solves this**:
- One command to switch: `ccs glm` or `ccs son`
- One command to switch: `ccs` (default) or `ccs glm` (fallback)
- Keep both configs saved as profiles
- Switch in <1 second
- No file editing, no copy-paste, no mistakes
@@ -110,6 +118,31 @@ If you have both Claude subscription and GLM Coding Plan, you know the pain:
- Auto-creates configs during install
- No proxies, no magic—just bash + jq
## New: Task Delegation
**CCS now includes intelligent task delegation** via the `/ccs` meta-command:
```bash
# Delegate planning to GLM (saves Sonnet tokens)
/ccs glm /plan "add user authentication"
# Delegate coding to GLM
/ccs glm /code "implement auth endpoints"
# Quick questions with Haiku
/ccs haiku /ask "explain this error"
```
**Documentation**:
- Command reference: [`commands/ccs.md`](commands/ccs.md)
- Delegation patterns: [`skills/ccs-delegation.md`](skills/ccs-delegation.md)
**Benefits**:
- ✅ Save tokens by delegating simple tasks to cheaper models
- ✅ Use right model for each task automatically
- ✅ Reusable commands across all projects (user-scope)
- ✅ Seamless integration with existing workflows
## Installation
### One-Liner (Recommended)
@@ -120,7 +153,7 @@ If you have both Claude subscription and GLM Coding Plan, you know the pain:
curl -fsSL ccs.kaitran.ca/install | bash
# Or direct from GitHub
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/install.sh | bash
```
**Windows PowerShell**:
@@ -129,7 +162,7 @@ curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/install.sh | ba
irm ccs.kaitran.ca/install.ps1 | iex
# Or direct from GitHub
irm https://raw.githubusercontent.com/kaitranntt/ccs/main/install.ps1 | iex
irm https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/install.ps1 | iex
```
**Note**:
@@ -142,14 +175,14 @@ irm https://raw.githubusercontent.com/kaitranntt/ccs/main/install.ps1 | iex
```bash
git clone https://github.com/kaitranntt/ccs.git
cd ccs
./install.sh
./installers/install.sh
```
**Windows PowerShell**:
```powershell
git clone https://github.com/kaitranntt/ccs.git
cd ccs
.\install.ps1
.\installers\install.ps1
```
**Note**: Works with git worktrees and submodules - the installer detects both `.git` directory and `.git` file.
@@ -201,8 +234,6 @@ git pull
irm ccs.kaitran.ca/install.ps1 | iex
```
**Note**: Upgrading preserves your existing API keys and settings. The installer only adds new features without overwriting your configuration.
## Configuration
The installer auto-creates config and profile templates during installation:
@@ -218,7 +249,6 @@ Uses settings file paths:
{
"profiles": {
"glm": "~/.ccs/glm.settings.json",
"sonnet": "~/.ccs/sonnet.settings.json",
"default": "~/.claude/settings.json"
}
}
@@ -228,16 +258,13 @@ Each profile points to a Claude settings JSON file. Create settings files per [C
### Windows Configuration
**Important**: Windows Claude CLI uses **environment variables** instead of --settings flag.
Windows uses the same file structure as Linux, but settings files contain environment variables:
Windows uses the same file structure and approach as Linux/macOS.
**Config format** (`~/.ccs/config.json`):
```json
{
"profiles": {
"glm": "~/.ccs/glm.settings.json",
"son": "~/.ccs/sonnet.settings.json",
"default": "~/.claude/settings.json"
}
}
@@ -246,29 +273,25 @@ Windows uses the same file structure as Linux, but settings files contain enviro
**GLM profile** (`~/.ccs/glm.settings.json`):
```json
{
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"ANTHROPIC_AUTH_TOKEN": "your_glm_api_key",
"ANTHROPIC_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
"env": {
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"ANTHROPIC_AUTH_TOKEN": "your_glm_api_key",
"ANTHROPIC_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
}
}
```
**Claude profile** (`~/.ccs/sonnet.settings.json`):
```json
{
"env": {}
}
```
**Claude (Default)**:
- Uses `~/.claude/settings.json` (your existing Claude CLI config)
- CCS never modifies this file (non-invasive approach)
**How it works**:
- CCS reads the settings file for the selected profile
- Temporarily sets environment variables from the settings file
- Executes Claude CLI with those variables
- Restores original environment variables after execution
**Compatibility**: Settings files support both direct format (Windows) and `{"env": {...}}` wrapper (Linux compatibility).
- CCS reads the config to find your profile's settings file
- Executes `claude --settings <file>` with your selected profile
- Simple, clean, cross-platform
## Usage
@@ -276,9 +299,8 @@ Windows uses the same file structure as Linux, but settings files contain enviro
```bash
# Works on macOS, Linux, and Windows
ccs # Use default profile (no args)
ccs glm # Use GLM profile
ccs son # Use Sonnet profile
ccs # Use Claude subscription (default)
ccs glm # Use GLM fallback
```
**Windows Note**: Commands work identically in PowerShell, CMD, and Git Bash.
@@ -289,8 +311,8 @@ All args after profile name pass directly to Claude CLI:
```bash
ccs glm --verbose
ccs son /plan "add feature"
ccs default --model claude-sonnet-4
ccs /plan "add feature"
ccs glm /code "implement feature"
```
### Custom Config Location
@@ -308,7 +330,7 @@ ccs glm
```bash
# Step 1: Architecture & Planning (needs Claude's intelligence)
ccs son
ccs
/plan "Design payment integration with Stripe, handle webhooks, errors, retries"
# → Claude Sonnet 4.5 thinks deeply about edge cases, security, architecture
@@ -318,7 +340,7 @@ ccs glm
# → GLM 4.6 writes the code efficiently, saves Claude usage
# Step 3: Code Review (needs deep analysis)
ccs son
ccs
/review "check the payment handler for security issues"
# → Claude Sonnet 4.5 catches subtle vulnerabilities
@@ -334,7 +356,7 @@ ccs glm
```bash
# Working on complex refactoring with Claude
ccs son
ccs
/plan "refactor authentication system"
# Claude hits rate limit mid-task
@@ -345,7 +367,7 @@ ccs glm
# Continue working without interruption
# Rate limit resets? Switch back
ccs son
ccs
```
### Configuration Examples
@@ -355,19 +377,18 @@ ccs son
{
"profiles": {
"glm": "~/.ccs/glm.settings.json",
"sonnet": "~/.ccs/sonnet.settings.json",
"default": "~/.claude/settings.json"
}
}
```
**Advanced setup** (multiple providers):
**Advanced setup** (multiple profiles):
```json
{
"profiles": {
"sonnet": "~/.ccs/sonnet.settings.json",
"glm": "~/.ccs/glm.settings.json",
"haiku": "~/.ccs/haiku.settings.json",
"custom": "~/.ccs/custom.settings.json",
"default": "~/.claude/settings.json"
}
}
@@ -379,7 +400,7 @@ ccs son
2. Looks up settings file path in `~/.ccs/config.json`
3. Executes `claude --settings <path> [remaining-args]`
No magic. No file modification. Pure delegation.
No magic. No file modification. Pure delegation. Works identically across all platforms.
## Requirements
@@ -458,7 +479,7 @@ This error occurs when running the installer in some shells or environments.
**Solution**: Upgrade to the latest version:
```bash
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/install.sh | bash
```
#### Git worktree not detected
@@ -533,14 +554,6 @@ Error: Profile 'default' not found in ~/.ccs/config.json
}
```
### Upgrade Issues
#### API keys lost after upgrade
**Not a problem**: The installer preserves existing API keys when upgrading. If you're using GLM, your API key is automatically preserved and the profile is enhanced with new default model variables.
**Verification**: Check `~/.ccs/glm.settings.json` - your `ANTHROPIC_AUTH_TOKEN` should still be present.
## Uninstallation
### macOS / Linux
@@ -556,7 +569,7 @@ ccs-uninstall
curl -fsSL ccs.kaitran.ca/uninstall | bash
# Or direct from GitHub
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/uninstall.sh | bash
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/uninstall.sh | bash
```
**Manual**:
@@ -579,7 +592,7 @@ ccs-uninstall
irm ccs.kaitran.ca/uninstall.ps1 | iex
# Or direct from GitHub
irm https://raw.githubusercontent.com/kaitranntt/ccs/main/uninstall.ps1 | iex
irm https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/uninstall.ps1 | iex
```
**Manual**:
+18 -21
View File
@@ -111,7 +111,7 @@ Nếu bạn có cả Claude subscription và GLM Coding Plan, bạn biết cái
curl -fsSL ccs.kaitran.ca/install | bash
# Hoặc trực tiếp từ GitHub
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/install.sh | bash
```
**Windows PowerShell**:
@@ -120,7 +120,7 @@ curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/install.sh | ba
irm ccs.kaitran.ca/install.ps1 | iex
# Hoặc trực tiếp từ GitHub
irm https://raw.githubusercontent.com/kaitranntt/ccs/main/install.ps1 | iex
irm https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/install.ps1 | iex
```
**Lưu ý**:
@@ -133,14 +133,14 @@ irm https://raw.githubusercontent.com/kaitranntt/ccs/main/install.ps1 | iex
```bash
git clone https://github.com/kaitranntt/ccs.git
cd ccs
./install.sh
./installers/install.sh
```
**Windows PowerShell**:
```powershell
git clone https://github.com/kaitranntt/ccs.git
cd ccs
.\install.ps1
.\installers\install.ps1
```
**Lưu ý**: Hoạt động với git worktrees và submodules - installer phát hiện cả `.git` directory và `.git` file.
@@ -231,9 +231,7 @@ Mỗi profile trỏ đến một file settings JSON của Claude. Tạo file set
### Cấu Hình Windows
**Quan trọng**: Claude CLI trên Windows dùng **biến môi trường** thay vì --settings flag.
Windows dùng cùng cấu trúc file như Linux, nhưng settings files chứa environment variables:
Windows dùng cùng cấu trúc file và phương pháp như Linux/macOS.
**Config format** (`~/.ccs/config.json`):
```json
@@ -249,12 +247,14 @@ Windows dùng cùng cấu trúc file như Linux, nhưng settings files chứa en
**GLM profile** (`~/.ccs/glm.settings.json`):
```json
{
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"ANTHROPIC_AUTH_TOKEN": "GLM_API_KEY_CUA_BAN",
"ANTHROPIC_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
"env": {
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"ANTHROPIC_AUTH_TOKEN": "GLM_API_KEY_CUA_BAN",
"ANTHROPIC_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
}
}
```
@@ -266,12 +266,9 @@ Windows dùng cùng cấu trúc file như Linux, nhưng settings files chứa en
```
**Cách hoạt động**:
- CCS đọc settings file của profile được chọn
- Tạm thời set biến môi trường từ settings file
- Chạy Claude CLI với các biến đó
- Khôi phục biến môi trường gốc sau khi thực thi
**Tương thích**: Settings files hỗ trợ cả format trực tiếp (Windows) và wrapper `{"env": {...}}` (tương thích Linux).
- CCS đọc config để tìm settings file của profile
- Chạy `claude --settings <file>` với profile đã chọn
- Đơn giản, rõ ràng, đa nền tảng
## Sử Dụng
@@ -554,7 +551,7 @@ ccs-uninstall
curl -fsSL ccs.kaitran.ca/uninstall | bash
# Hoặc trực tiếp từ GitHub
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/uninstall.sh | bash
curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/uninstall.sh | bash
```
**Thủ công**:
@@ -577,7 +574,7 @@ ccs-uninstall
irm ccs.kaitran.ca/uninstall.ps1 | iex
# Hoặc trực tiếp từ GitHub
irm https://raw.githubusercontent.com/kaitranntt/ccs/main/uninstall.ps1 | iex
irm https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/uninstall.ps1 | iex
```
**Thủ công**:
+1
View File
@@ -0,0 +1 @@
2.1.1
+24 -11
View File
@@ -1,31 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
# Version
CCS_VERSION="1.1.0"
# Version - Read from VERSION file
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CCS_VERSION="$(cat "$SCRIPT_DIR/VERSION" 2>/dev/null || echo "unknown")"
CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
PROFILE="${1:-default}"
# Special case: version command
if [[ "$PROFILE" == "version" || "$PROFILE" == "--version" || "$PROFILE" == "-v" ]]; then
# Special case: version command (check BEFORE profile detection)
if [[ $# -gt 0 ]] && [[ "${1}" == "version" || "${1}" == "--version" || "${1}" == "-v" ]]; then
echo "CCS (Claude Code Switch) version $CCS_VERSION"
echo "https://github.com/kaitranntt/ccs"
exit 0
fi
# Special case: help command (forward to Claude CLI)
if [[ "$PROFILE" == "--help" || "$PROFILE" == "-h" || "$PROFILE" == "help" ]]; then
# Special case: help command (check BEFORE profile detection)
if [[ $# -gt 0 ]] && [[ "${1}" == "--help" || "${1}" == "-h" || "${1}" == "help" ]]; then
shift # Remove the help argument
exec claude --help "$@"
fi
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
if [[ $# -eq 0 ]] || [[ "${1}" =~ ^- ]]; then
# No args or first arg is a flag → use default profile
PROFILE="default"
else
# First arg doesn't start with '-' → treat as profile name
PROFILE="${1}"
fi
# Check config exists
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Error: Config file not found: $CONFIG_FILE"
echo ""
echo "Create ~/.ccs/config.json with your profile mappings."
echo "See .ccs.example.json for template."
echo "See config/config.example.json for template."
echo ""
echo "Or reinstall: curl -fsSL ccs.kaitran.ca/install | bash"
exit 1
fi
@@ -55,7 +66,9 @@ 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"
echo ""
echo "See config/config.example.json for correct format"
echo "Or reinstall: curl -fsSL ccs.kaitran.ca/install | bash"
exit 1
fi
@@ -81,8 +94,8 @@ if [[ ! -f "$SETTINGS_PATH" ]]; then
exit 1
fi
# Shift profile arg if it was provided, pass rest to claude
if [[ $# -gt 0 ]]; then
# Shift profile arg only if first arg was NOT a flag
if [[ $# -gt 0 ]] && [[ ! "${1}" =~ ^- ]]; then
shift
fi
+41 -64
View File
@@ -4,7 +4,7 @@
param(
[Parameter(Position=0)]
[string]$Profile = "default",
[string]$ProfileOrFlag = "default",
[Parameter(ValueFromRemainingArguments=$true)]
[string[]]$RemainingArgs
@@ -12,18 +12,26 @@ param(
$ErrorActionPreference = "Stop"
# Version
$CCS_VERSION = "1.1.0"
# Version - Read from VERSION file
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$VersionFile = Join-Path $ScriptDir "VERSION"
$CCS_VERSION = if (Test-Path $VersionFile) {
(Get-Content $VersionFile -Raw).Trim()
} else {
"unknown"
}
# Special case: version command
if ($Profile -eq "version" -or $Profile -eq "--version" -or $Profile -eq "-v") {
# Special case: version command (check BEFORE profile detection)
# Check both $ProfileOrFlag and first element of $RemainingArgs
$FirstArg = if ($ProfileOrFlag -ne "default") { $ProfileOrFlag } elseif ($RemainingArgs.Count -gt 0) { $RemainingArgs[0] } else { $null }
if ($FirstArg -eq "version" -or $FirstArg -eq "--version" -or $FirstArg -eq "-v") {
Write-Host "CCS (Claude Code Switch) version $CCS_VERSION"
Write-Host "https://github.com/kaitranntt/ccs"
exit 0
}
# Special case: help command (forward to Claude CLI)
if ($Profile -eq "--help" -or $Profile -eq "-h" -or $Profile -eq "help") {
# Special case: help command (check BEFORE profile detection)
if ($FirstArg -eq "--help" -or $FirstArg -eq "-h" -or $FirstArg -eq "help") {
try {
if ($RemainingArgs) {
& claude --help @RemainingArgs
@@ -38,6 +46,22 @@ if ($Profile -eq "--help" -or $Profile -eq "-h" -or $Profile -eq "help") {
}
}
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
if ($ProfileOrFlag -match '^-') {
# First arg is a flag → use default profile, keep all args
$Profile = "default"
# Prepend $ProfileOrFlag to $RemainingArgs (it's actually a flag, not a profile)
if ($RemainingArgs) {
$RemainingArgs = @($ProfileOrFlag) + $RemainingArgs
} else {
$RemainingArgs = @($ProfileOrFlag)
}
} else {
# First arg is a profile name
$Profile = $ProfileOrFlag
# $RemainingArgs already contains correct args (PowerShell handles this)
}
# Special case: "default" profile just runs claude directly (no profile switching)
if ($Profile -eq "default") {
try {
@@ -122,7 +146,7 @@ if (-not (Test-Path $SettingsPath)) {
Write-Host "Error: Settings file not found: $SettingsPath" -ForegroundColor Red
Write-Host ""
Write-Host "Solutions:" -ForegroundColor Yellow
Write-Host " 1. Create the settings file with empty env:"
Write-Host " 1. Create the settings file:"
Write-Host " New-Item -ItemType File -Force -Path '$SettingsPath'"
Write-Host " Set-Content -Path '$SettingsPath' -Value '{`"env`":{}}`'"
Write-Host ""
@@ -132,7 +156,7 @@ if (-not (Test-Path $SettingsPath)) {
exit 1
}
# Read settings file (contains environment variables for Windows)
# Validate settings file is valid JSON (basic check)
try {
$SettingsContent = Get-Content $SettingsPath -Raw -ErrorAction Stop
$Settings = $SettingsContent | ConvertFrom-Json -ErrorAction Stop
@@ -150,63 +174,16 @@ try {
exit 1
}
# Windows Claude CLI uses environment variables instead of --settings flag
# Settings file contains env vars directly or nested in "env" object (Linux compat)
# Check if settings contain "env" wrapper (Linux format) or direct env vars (Windows format)
$EnvVars = if ($Settings.env) {
# Linux format: { "env": { "ANTHROPIC_AUTH_TOKEN": "..." } }
$Settings.env
} else {
# Windows format: { "ANTHROPIC_AUTH_TOKEN": "..." }
$Settings
}
# Save original environment variables to restore after execution
$OriginalEnvVars = @{}
if ($EnvVars.PSObject.Properties.Count -gt 0) {
foreach ($Property in $EnvVars.PSObject.Properties) {
$VarName = $Property.Name
$VarValue = $Property.Value
try {
# Save original value
$OriginalEnvVars[$VarName] = [System.Environment]::GetEnvironmentVariable($VarName, [System.EnvironmentVariableTarget]::Process)
# Set new value for this process only
[System.Environment]::SetEnvironmentVariable($VarName, $VarValue, [System.EnvironmentVariableTarget]::Process)
} catch {
Write-Host "Warning: Could not set environment variable '$VarName'" -ForegroundColor Yellow
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
}
# Execute claude with environment variables set
# Execute claude with settings file (using --settings flag)
try {
if ($RemainingArgs) {
& claude @RemainingArgs
& claude --settings $SettingsPath @RemainingArgs
} else {
& claude
}
$ExitCode = $LASTEXITCODE
} finally {
# Restore original environment variables
foreach ($VarName in $OriginalEnvVars.Keys) {
$OriginalValue = $OriginalEnvVars[$VarName]
try {
if ($null -eq $OriginalValue) {
# Variable didn't exist before, remove it
[System.Environment]::SetEnvironmentVariable($VarName, $null, [System.EnvironmentVariableTarget]::Process)
} else {
# Restore original value
[System.Environment]::SetEnvironmentVariable($VarName, $OriginalValue, [System.EnvironmentVariableTarget]::Process)
}
} catch {
Write-Host "Warning: Could not restore environment variable '$VarName'" -ForegroundColor Yellow
}
& claude --settings $SettingsPath
}
exit $LASTEXITCODE
} catch {
Write-Host "Error: Failed to execute claude" -ForegroundColor Red
Write-Host $_.Exception.Message
exit 1
}
exit $ExitCode
+10
View File
@@ -0,0 +1,10 @@
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"ANTHROPIC_AUTH_TOKEN": "YOUR_GLM_API_KEY_HERE",
"ANTHROPIC_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
}
}
@@ -1,8 +1,6 @@
{
"profiles": {
"glm": "~/.ccs/glm.settings.json",
"sonnet": "~/.ccs/sonnet.settings.json",
"default": "~/.claude/settings.json"
}
}
+191
View File
@@ -0,0 +1,191 @@
# CCS Workflow Documentation
Workflow documentation for CCS v2.0.0 covering installation, runtime behavior, and troubleshooting.
---
## Architecture Overview
```
User Command: ccs [profile] [claude-args...]
┌─────────────────┐
│ ccs (wrapper) │ ← Bash (Unix) or PowerShell (Windows)
└────────┬────────┘
├─ 1. Read ~/.ccs/config.json
├─ 2. Lookup profile → settings file
├─ 3. Validate settings file exists
┌────────────────┐
│ Profile System │
└───────┬────────┘
├─ default: ~/.claude/settings.json
└─ glm: ~/.ccs/glm.settings.json
┌──────────────────┐
│ Claude CLI │
└──────────────────┘
```
### Key Components
1. **ccs wrapper**: Lightweight script (bash/PowerShell)
2. **Config file**: `~/.ccs/config.json` (profile → settings mappings)
3. **Settings files**: Claude CLI settings JSON format
4. **Claude CLI**: Official Anthropic CLI (unchanged)
### Design Principles
- **YAGNI**: Only 2 profiles (default/glm)
- **KISS**: Simple delegation, no magic
- **DRY**: One source of truth (config.json)
- **Non-invasive**: Never modifies ~/.claude/settings.json
---
## Installation Workflow
### Process
1. **Create directories**: `~/.ccs/`, `~/.local/bin/`
2. **Install executables**:
- Git mode: Symlink from repo
- Standalone: Download from GitHub
3. **Install .claude folder**: Commands and skills
4. **Create config**: `config.json` if missing
5. **Create GLM profile**: `glm.settings.json` if missing
6. **Backup**: Single `config.json.backup` (overwrites)
7. **Validate**: Check JSON syntax
### Files Created
```
~/.ccs/
├── ccs # Main executable
├── config.json # Profile mappings
├── config.json.backup # Single backup (no timestamp)
├── glm.settings.json # GLM profile
├── uninstall.sh # Uninstaller
└── .claude/ # Claude Code integration
├── commands/ccs.md
└── skills/ccs-delegation/
```
---
## Runtime Workflow
### Unix/Linux/macOS
```
ccs [profile] [args]
Read config.json
Lookup profile → settings file path
Validate file exists
exec claude --settings <path> [args]
```
### Windows
```
ccs [profile] [args]
Read config.json
Lookup profile → settings file path
Validate file exists
exec claude --settings <path> [args]
```
---
## Profile System
### Config Structure
```json
{
"profiles": {
"glm": "~/.ccs/glm.settings.json",
"default": "~/.claude/settings.json"
}
}
```
### Settings File Format
**GLM Profile** (`~/.ccs/glm.settings.json`):
```json
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"ANTHROPIC_AUTH_TOKEN": "your_api_key",
"ANTHROPIC_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
}
}
```
**Claude (Default)** (`~/.claude/settings.json`):
- User-managed, CCS never modifies it
- Referenced by default profile
---
## Troubleshooting
### Profile Not Found
| Check | Fix |
|-------|-----|
| config.json exists? | Run installer |
| Valid JSON? | Fix syntax or restore from backup |
| Profile in config? | Add profile or use default/glm |
| Settings file exists? | Create from template |
### PowerShell Crash (Windows)
**Error**: `SetEnvironmentVariable` error
**Fix**:
1. Check settings format has `{"env": {...}}`
2. Ensure all values are strings (not booleans/objects)
3. Remove non-env fields from settings file
### Installation Issues
| Issue | Fix |
|-------|-----|
| 404 Not Found | Check installers exist in GitHub |
| Permission denied | Check ~/.ccs/ permissions |
| jq not found | Install jq: `brew install jq` (Unix) |
| PATH warning | Add `~/.local/bin` to PATH |
---
## Key Points
1. **Installation**: Creates 2 profiles (glm + default), validates JSON
2. **Runtime**: Simple delegation to Claude CLI via `--settings` flag
3. **Cross-platform**: Identical behavior on Unix/Linux/macOS/Windows
4. **Non-invasive**: Never touches `~/.claude/settings.json`
5. **Validation**: JSON syntax checking prevents errors
6. **Backup**: Single file, overwrites each install
---
**Version**: v2.0.0
**Updated**: 2025-11-02
+101
View File
@@ -0,0 +1,101 @@
# Version Management
## Overview
CCS uses a centralized version management system to ensure consistency across all components.
## Version Locations
The version number must be kept in sync across these files:
1. **`VERSION`** - Primary version file (read by ccs/ccs.ps1 at runtime)
2. **`installers/install.sh`** - Hardcoded for standalone installations (`curl | bash`)
3. **`installers/install.ps1`** - Hardcoded for standalone installations (`irm | iex`)
## Why Hardcoded Versions in Installers?
When users run:
- `curl -fsSL ccs.kaitran.ca/install | bash`
- `irm ccs.kaitran.ca/install.ps1 | iex`
The installer script is downloaded and executed directly **without** the VERSION file. Therefore, installers must have a hardcoded version as fallback.
For git-based installations, the VERSION file is read if available, overriding the hardcoded version.
## Updating Version
### Automated Method (Recommended)
Use the provided script to bump the version automatically:
```bash
# Bump patch version (2.1.1 -> 2.1.2)
./scripts/bump-version.sh patch
# Bump minor version (2.1.1 -> 2.2.0)
./scripts/bump-version.sh minor
# Bump major version (2.1.1 -> 3.0.0)
./scripts/bump-version.sh major
```
This updates:
- VERSION file
- installers/install.sh (hardcoded version)
- installers/install.ps1 (hardcoded version)
- Creates git tag (if in git repo)
### Manual Method
If updating manually, update version in ALL three locations:
1. **VERSION file**:
```bash
echo "2.1.2" > VERSION
```
2. **installers/install.sh** (line ~34):
```bash
CCS_VERSION="2.1.2"
```
3. **installers/install.ps1** (line ~33):
```powershell
$CcsVersion = "2.1.2"
```
## Release Checklist
When releasing a new version:
- [ ] Update version using `./scripts/update-version.sh X.Y.Z`
- [ ] Review changes: `git diff`
- [ ] Update CHANGELOG.md with release notes
- [ ] Commit changes: `git commit -am "chore: bump version to X.Y.Z"`
- [ ] Create tag: `git tag -a vX.Y.Z -m "Release vX.Y.Z"`
- [ ] Push: `git push && git push --tags`
- [ ] Verify CloudFlare worker serves updated installer
## Version Display
After installation, users can check version:
```bash
# Shows CCS version (from VERSION file if available)
ccs --version
# Shows Claude CLI version
ccs version
```
## Semantic Versioning
CCS follows [Semantic Versioning](https://semver.org/):
- **MAJOR** (X.0.0): Breaking changes
- **MINOR** (0.X.0): New features (backward compatible)
- **PATCH** (0.0.X): Bug fixes
Current version: **2.1.1**
- 2.1.0: Added task delegation feature
- 2.1.1: Fixed argument parsing bug (flags treated as profiles)
+150 -77
View File
@@ -20,12 +20,33 @@ $ScriptDir = if ($MyInvocation.MyCommand.Path) {
$null
}
$InstallMethod = if ($ScriptDir -and (Test-Path "$ScriptDir\ccs.ps1")) {
$InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\ccs.ps1") -or (Test-Path "$ScriptDir\..\ccs.ps1"))) {
"git"
} else {
"standalone"
}
# Version configuration
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (irm | iex)
# For git installations, VERSION file is read if available
$CcsVersion = "2.1.1"
# Try to read VERSION file for git installations
if ($ScriptDir) {
$VersionFile = if (Test-Path "$ScriptDir\VERSION") {
"$ScriptDir\VERSION"
} elseif (Test-Path "$ScriptDir\..\VERSION") {
"$ScriptDir\..\VERSION"
} else {
$null
}
if ($VersionFile -and (Test-Path $VersionFile)) {
$CcsVersion = (Get-Content $VersionFile -Raw).Trim()
}
}
# Helper Functions
function Detect-CurrentProvider {
@@ -62,11 +83,6 @@ function New-GlmTemplate {
return $Template | ConvertTo-Json -Depth 10
}
function New-SonnetTemplate {
$Template = @{ env = @{} }
return $Template | ConvertTo-Json -Depth 10
}
function New-GlmProfile {
param([string]$Provider)
@@ -86,7 +102,7 @@ function New-GlmProfile {
$Config.env | Add-Member -NotePropertyName ANTHROPIC_DEFAULT_HAIKU_MODEL -NotePropertyValue $GlmModel -Force
$Config | ConvertTo-Json -Depth 10 | Set-Content $GlmSettings
Write-Host " Created: $GlmSettings (with your existing API key + enhanced settings)"
Write-Host " Created: $GlmSettings with your existing API key + enhanced settings"
} catch {
Write-Host " [i] Copying current settings failed, using template"
New-GlmTemplate | Set-Content $GlmSettings
@@ -99,44 +115,63 @@ function New-GlmProfile {
}
}
function New-SonnetProfile {
param([string]$Provider)
function Install-ClaudeFolder {
param(
[string]$SourceDir
)
$CurrentSettings = "$ClaudeDir\settings.json"
$SonnetSettings = "$CcsDir\sonnet.settings.json"
$TargetDir = "$CcsDir\.claude"
if ($Provider -eq "claude" -and (Test-Path $CurrentSettings)) {
Write-Host "[OK] Copying current Claude config to profile..."
Copy-Item $CurrentSettings $SonnetSettings
Write-Host " Created: $SonnetSettings"
} else {
Write-Host "Creating Claude Sonnet profile template at $SonnetSettings"
# Check if already exists
if (Test-Path $TargetDir) {
Write-Host "| [i] .claude/ folder already exists, skipping"
return $true
}
if (Test-Path $CurrentSettings) {
# Create directory structure
$null = New-Item -ItemType Directory -Force -Path "$TargetDir\commands"
$null = New-Item -ItemType Directory -Force -Path "$TargetDir\skills\ccs-delegation\references"
if ($InstallMethod -eq "git" -and $SourceDir) {
# Copy from local git repo
$SourceClaudeDir = Join-Path $SourceDir ".claude"
if (Test-Path $SourceClaudeDir) {
try {
$Config = Get-Content $CurrentSettings -Raw | ConvertFrom-Json
# Remove GLM-specific vars
if ($Config.env) {
$Config.env.PSObject.Properties.Remove('ANTHROPIC_BASE_URL')
$Config.env.PSObject.Properties.Remove('ANTHROPIC_AUTH_TOKEN')
$Config.env.PSObject.Properties.Remove('ANTHROPIC_MODEL')
}
$Config | ConvertTo-Json -Depth 10 | Set-Content $SonnetSettings
Copy-Item -Path "$SourceClaudeDir\*" -Destination $TargetDir -Recurse -Force
Write-Host "| [OK] Installed .claude/ folder"
return $true
} catch {
New-SonnetTemplate | Set-Content $SonnetSettings
Write-Host "| [!] Failed to copy .claude/ folder"
return $false
}
} else {
New-SonnetTemplate | Set-Content $SonnetSettings
Write-Host "| [!] .claude/ folder not found in source"
return $false
}
} else {
# Standalone: download from GitHub
try {
$BaseUrl = "https://raw.githubusercontent.com/kaitranntt/ccs/main/.claude"
Write-Host " Created: $SonnetSettings"
Write-Host " [i] This uses your Claude subscription (no API key needed)"
Invoke-WebRequest -Uri "$BaseUrl/commands/ccs.md" `
-OutFile "$TargetDir\commands\ccs.md" -UseBasicParsing
Invoke-WebRequest -Uri "$BaseUrl/skills/ccs-delegation/SKILL.md" `
-OutFile "$TargetDir\skills\ccs-delegation\SKILL.md" -UseBasicParsing
Invoke-WebRequest -Uri "$BaseUrl/skills/ccs-delegation/references/delegation-patterns.md" `
-OutFile "$TargetDir\skills\ccs-delegation\references\delegation-patterns.md" -UseBasicParsing
Write-Host "| [OK] Downloaded .claude/ folder"
return $true
} catch {
Write-Host "| [!] Failed to download .claude/ folder"
return $false
}
}
}
# Main Installation
Write-Host "┌─ Installing CCS (Windows)"
Write-Host '===== Installing CCS (Windows) ====='
# Create directories
New-Item -ItemType Directory -Force -Path $CcsDir | Out-Null
@@ -158,8 +193,28 @@ if ($InstallMethod -eq "standalone") {
}
} else {
# Git install - copy local file
Copy-Item "$ScriptDir\ccs.ps1" "$CcsDir\ccs.ps1" -Force
$CcsPs1Path = if (Test-Path "$ScriptDir\ccs.ps1") {
"$ScriptDir\ccs.ps1"
} elseif (Test-Path "$ScriptDir\..\ccs.ps1") {
"$ScriptDir\..\ccs.ps1"
} else {
throw "ccs.ps1 not found"
}
Copy-Item $CcsPs1Path "$CcsDir\ccs.ps1" -Force
Write-Host "| [OK] Installed ccs.ps1"
# Copy VERSION file if available (for proper version display)
$VersionPath = if (Test-Path "$ScriptDir\VERSION") {
"$ScriptDir\VERSION"
} elseif (Test-Path "$ScriptDir\..\VERSION") {
"$ScriptDir\..\VERSION"
} else {
$null
}
if ($VersionPath) {
Copy-Item $VersionPath "$CcsDir\VERSION" -Force
Write-Host "| [OK] Installed VERSION file"
}
}
# Install uninstall script as ccs-uninstall.ps1
@@ -189,7 +244,16 @@ if ($ScriptDir -and (Test-Path "$ScriptDir\uninstall.ps1")) {
}
Write-Host "| [OK] Created directories"
Write-Host "└─"
# Install .claude/ folder
if ($InstallMethod -eq "git" -and $ScriptDir) {
$ParentDir = Split-Path -Parent $ScriptDir
$null = Install-ClaudeFolder -SourceDir $ParentDir
} else {
$null = Install-ClaudeFolder -SourceDir ""
}
Write-Host "========================================="
Write-Host ""
# Profile Setup
@@ -197,50 +261,68 @@ Write-Host ""
$CurrentProvider = Detect-CurrentProvider
$ProviderLabel = switch ($CurrentProvider) {
"glm" { " (detected: GLM)" }
"claude" { " (detected: Claude)" }
"custom" { " (detected: custom)" }
"glm" { ' (detected: GLM)' }
"claude" { ' (detected: Claude)' }
"custom" { ' (detected: custom)' }
default { "" }
}
Write-Host "┌─ Configuring Profiles$ProviderLabel"
Write-Host "===== Configuring Profiles v$CcsVersion$ProviderLabel"
$NeedsGlmKey = $false
# Create ccs config with file paths (same structure as Linux)
# Backup existing config (single backup, no timestamp)
$ConfigFile = "$CcsDir\config.json"
if (-not (Test-Path $ConfigFile)) {
$ConfigContent = @{
profiles = @{
glm = "~/.ccs/glm.settings.json"
son = "~/.ccs/sonnet.settings.json"
}
}
$ConfigContent | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile
Write-Host "| [OK] Config -> $env:USERPROFILE\.ccs\config.json"
$BackupFile = "$CcsDir\config.json.backup"
if (Test-Path $ConfigFile) {
Copy-Item $ConfigFile $BackupFile -Force
}
# Create profile settings files
$NeedsGlmKey = $false
$GlmSettings = "$CcsDir\glm.settings.json"
$SonnetSettings = "$CcsDir\sonnet.settings.json"
# Create GLM profile if missing
if (-not (Test-Path $GlmSettings)) {
New-GlmProfile -Provider $CurrentProvider
if ($CurrentProvider -ne "glm") {
$NeedsGlmKey = $true
}
} else {
Write-Host "| [OK] GLM profile exists: $GlmSettings"
Write-Host '| [OK] GLM profile exists'
}
if (-not (Test-Path $SonnetSettings)) {
New-SonnetProfile -Provider $CurrentProvider
} else {
Write-Host "| [OK] Sonnet profile exists: $SonnetSettings"
# Create config if missing
if (-not (Test-Path $ConfigFile)) {
$ConfigContent = @{
profiles = @{
glm = "~/.ccs/glm.settings.json"
default = "~/.claude/settings.json"
}
}
$ConfigContent | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile
Write-Host ('| OK: Config created at {0}\.ccs\config.json' -f $env:USERPROFILE)
}
Write-Host "└─"
# Validate config JSON
if (Test-Path $ConfigFile) {
try {
$null = Get-Content $ConfigFile -Raw | ConvertFrom-Json
} catch {
Write-Host '| [!] Warning: Invalid JSON in config.json' -ForegroundColor Yellow
if (Test-Path $BackupFile) {
Write-Host ('| Restore from: {0}' -f $BackupFile)
}
}
}
# Validate GLM settings JSON
if (Test-Path $GlmSettings) {
try {
$null = Get-Content $GlmSettings -Raw | ConvertFrom-Json
} catch {
Write-Host '| [!] Warning: Invalid JSON in glm.settings.json' -ForegroundColor Yellow
}
}
Write-Host "========================================="
Write-Host ""
# Check and update PATH
@@ -274,25 +356,16 @@ if ($NeedsGlmKey) {
Write-Host "[SUCCESS] CCS installed successfully!"
Write-Host ""
# Build quick start based on current provider
if ($CurrentProvider -eq "claude") {
Write-Host " Quick start:"
Write-Host " ccs son # Claude Sonnet (current)"
Write-Host " ccs glm # GLM (after adding API key)"
Write-Host " ccs # Default (no profile)"
} elseif ($CurrentProvider -eq "glm") {
Write-Host " Quick start:"
Write-Host " ccs glm # GLM (current)"
Write-Host " ccs son # Claude Sonnet"
Write-Host " ccs # Default (no profile)"
} else {
Write-Host " Quick start:"
Write-Host " ccs # Default (no profile)"
Write-Host " ccs son # Claude Sonnet"
Write-Host " ccs glm # GLM (after adding API key)"
}
Write-Host " Installed components:"
Write-Host " * ccs command -> $CcsDir\ccs.ps1"
Write-Host " * config -> $CcsDir\config.json"
Write-Host " * glm profile -> $CcsDir\glm.settings.json"
Write-Host " * .claude/ folder -> $CcsDir\.claude\"
Write-Host ""
Write-Host " Quick start:"
Write-Host " ccs # Use Claude subscription - default"
Write-Host " ccs glm # Use GLM fallback"
Write-Host ""
Write-Host ""
Write-Host " Usage: ccs [profile] [claude-args]"
Write-Host " Example: ccs glm /plan 'implement feature'"
+138 -82
View File
@@ -19,14 +19,27 @@ else
fi
# Detect installation method (git vs standalone)
# Check if ccs executable exists in SCRIPT_DIR (real git install)
# Check if ccs executable exists in SCRIPT_DIR or parent (real git install)
# Don't just check .git (user might run curl | bash inside their own git repo)
if [[ -f "$SCRIPT_DIR/ccs" ]]; then
if [[ -f "$SCRIPT_DIR/ccs" ]] || [[ -f "$SCRIPT_DIR/../ccs" ]]; then
INSTALL_METHOD="git"
else
INSTALL_METHOD="standalone"
fi
# Version configuration
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (curl | bash)
# For git installations, VERSION file is read if available
CCS_VERSION="2.1.1"
# Try to read VERSION file for git installations
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
CCS_VERSION="$(cat "$SCRIPT_DIR/VERSION" | tr -d '\n' | tr -d '\r')"
elif [[ -f "$SCRIPT_DIR/../VERSION" ]]; then
CCS_VERSION="$(cat "$SCRIPT_DIR/../VERSION" | tr -d '\n' | tr -d '\r')"
fi
# --- Platform Detection ---
# Detect platform and redirect to Windows installer if needed
detect_platform() {
@@ -91,14 +104,6 @@ create_glm_template() {
EOF
}
create_sonnet_template() {
cat << 'EOF'
{
"env": {}
}
EOF
}
atomic_mv() {
local src="$1"
local dest="$2"
@@ -106,11 +111,60 @@ atomic_mv() {
return 0
else
rm -f "$src"
echo " Error: Failed to create $dest (check permissions)"
echo " Error: Failed to create $dest (check permissions)"
exit 1
fi
}
download_file() {
local url="$1"
local dest="$2"
if ! curl -fsSL "$url" -o "$dest"; then
echo " ⚠ Failed to download: $(basename "$dest")"
return 1
fi
return 0
}
install_claude_folder() {
local source_dir="$1"
local target_dir="$CCS_DIR/.claude"
# Check if already exists
if [[ -d "$target_dir" ]]; then
echo "│ .claude/ folder already exists, skipping"
return 0
fi
mkdir -p "$target_dir/commands" "$target_dir/skills/ccs-delegation/references"
if [[ "$INSTALL_METHOD" == "git" ]]; then
# Copy from local git repo
if [[ -d "$source_dir/.claude" ]]; then
cp -r "$source_dir/.claude"/* "$target_dir/" 2>/dev/null || {
echo "│ ⚠ Failed to copy .claude/ folder"
return 1
}
echo "│ ✓ Installed .claude/ folder"
else
echo "│ ⚠ .claude/ folder not found in source"
return 1
fi
else
# Standalone: download from GitHub
local base_url="https://raw.githubusercontent.com/kaitranntt/ccs/main/.claude"
download_file "$base_url/commands/ccs.md" "$target_dir/commands/ccs.md" || return 1
download_file "$base_url/skills/ccs-delegation/SKILL.md" "$target_dir/skills/ccs-delegation/SKILL.md" || return 1
download_file "$base_url/skills/ccs-delegation/references/delegation-patterns.md" "$target_dir/skills/ccs-delegation/references/delegation-patterns.md" || return 1
echo "│ ✓ Downloaded .claude/ folder"
fi
return 0
}
create_glm_profile() {
local current_settings="$CLAUDE_DIR/settings.json"
local glm_settings="$CCS_DIR/glm.settings.json"
@@ -149,50 +203,14 @@ create_glm_profile() {
atomic_mv "$glm_settings.tmp" "$glm_settings"
else
rm -f "$glm_settings.tmp"
echo " jq failed, using basic template"
echo " jq failed, using basic template"
create_glm_template > "$glm_settings"
fi
else
create_glm_template > "$glm_settings"
fi
echo " Created: $glm_settings"
echo " ⚠ Edit this file and replace YOUR_GLM_API_KEY_HERE with your actual GLM API key"
fi
}
create_sonnet_profile() {
local current_settings="$CLAUDE_DIR/settings.json"
local sonnet_settings="$CCS_DIR/sonnet.settings.json"
local provider="$1"
if [[ "$provider" == "claude" ]]; then
echo "✓ Copying current Claude config to profile..."
cp "$current_settings" "$sonnet_settings"
echo " Created: $sonnet_settings"
else
echo "Creating Claude Sonnet profile template at $sonnet_settings"
if [[ -f "$current_settings" ]] && command -v jq &> /dev/null; then
# Remove GLM-specific vars, but keep ANTHROPIC_DEFAULT_* if they contain "claude" (user preference)
# Filter logic assumes Claude model IDs contain "claude" substring (case-insensitive)
if jq 'del(.env.ANTHROPIC_BASE_URL, .env.ANTHROPIC_AUTH_TOKEN, .env.ANTHROPIC_MODEL) |
.env |= with_entries(
select(
(.key | IN("ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL") | not) or
(.value | tostring? // "" | ascii_downcase | contains("claude"))
)
) |
if (.env | length) == 0 then .env = {} else . end' "$current_settings" > "$sonnet_settings.tmp" 2>/dev/null; then
atomic_mv "$sonnet_settings.tmp" "$sonnet_settings"
else
rm -f "$sonnet_settings.tmp"
echo " ️ jq failed, using basic template"
create_sonnet_template > "$sonnet_settings"
fi
else
create_sonnet_template > "$sonnet_settings"
fi
echo " Created: $sonnet_settings"
echo " ️ This uses your Claude subscription (no API key needed)"
echo " ⚠ Edit this file and replace YOUR_GLM_API_KEY_HERE with your actual GLM API key"
fi
}
@@ -207,7 +225,7 @@ mkdir -p "$INSTALL_DIR" "$CCS_DIR"
if [[ "$INSTALL_METHOD" == "standalone" ]]; then
# Standalone install - download ccs from GitHub
if ! command -v curl &> /dev/null; then
echo " Error: curl is required for standalone installation"
echo " Error: curl is required for standalone installation"
exit 1
fi
@@ -222,9 +240,28 @@ if [[ "$INSTALL_METHOD" == "standalone" ]]; then
fi
else
# Git install - use local ccs file
chmod +x "$SCRIPT_DIR/ccs"
ln -sf "$SCRIPT_DIR/ccs" "$INSTALL_DIR/ccs"
# Handle both running from root or from installers/ subdirectory
if [[ -f "$SCRIPT_DIR/ccs" ]]; then
chmod +x "$SCRIPT_DIR/ccs"
ln -sf "$SCRIPT_DIR/ccs" "$INSTALL_DIR/ccs"
elif [[ -f "$SCRIPT_DIR/../ccs" ]]; then
chmod +x "$SCRIPT_DIR/../ccs"
ln -sf "$SCRIPT_DIR/../ccs" "$INSTALL_DIR/ccs"
else
echo "│"
echo "✗ Error: ccs executable not found"
exit 1
fi
echo "│ ✓ Installed executable"
# Copy VERSION file if available (for proper version display)
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
cp "$SCRIPT_DIR/VERSION" "$CCS_DIR/VERSION"
echo "│ ✓ Installed VERSION file"
elif [[ -f "$SCRIPT_DIR/../VERSION" ]]; then
cp "$SCRIPT_DIR/../VERSION" "$CCS_DIR/VERSION"
echo "│ ✓ Installed VERSION file"
fi
fi
if [[ ! -L "$INSTALL_DIR/ccs" ]]; then
@@ -252,6 +289,14 @@ elif [[ "$INSTALL_METHOD" == "standalone" ]] && command -v curl &> /dev/null; th
fi
echo "│ ✓ Created directories"
# Install .claude/ folder
if [[ "$INSTALL_METHOD" == "git" ]]; then
install_claude_folder "$SCRIPT_DIR/.." || echo "│ ⚠ Optional .claude/ installation skipped"
else
install_claude_folder "" || echo "│ ⚠ Optional .claude/ installation skipped"
fi
echo "└─"
echo ""
@@ -259,7 +304,6 @@ echo ""
CURRENT_PROVIDER=$(detect_current_provider)
GLM_SETTINGS="$CCS_DIR/glm.settings.json"
SONNET_SETTINGS="$CCS_DIR/sonnet.settings.json"
# Build provider label
PROVIDER_LABEL=""
@@ -267,30 +311,30 @@ PROVIDER_LABEL=""
[[ "$CURRENT_PROVIDER" == "claude" ]] && PROVIDER_LABEL=" (detected: Claude)"
[[ "$CURRENT_PROVIDER" == "custom" ]] && PROVIDER_LABEL=" (detected: custom)"
echo "┌─ Configuring Profiles${PROVIDER_LABEL}"
echo "┌─ Configuring Profiles (v${CCS_VERSION})${PROVIDER_LABEL}"
# Backup existing config if present (single backup, no timestamp)
BACKUP_FILE="$CCS_DIR/config.json.backup"
if [[ -f "$CCS_DIR/config.json" ]]; then
cp "$CCS_DIR/config.json" "$BACKUP_FILE"
fi
# Track if GLM needs API key
NEEDS_GLM_KEY=false
# Create missing profiles (silently, show only result)
# Create GLM profile if missing
if [[ ! -f "$GLM_SETTINGS" ]]; then
create_glm_profile "$CURRENT_PROVIDER" >/dev/null 2>&1
echo "│ ✓ GLM profile → ~/.ccs/glm.settings.json"
[[ "$CURRENT_PROVIDER" != "glm" ]] && NEEDS_GLM_KEY=true
fi
if [[ ! -f "$SONNET_SETTINGS" ]]; then
create_sonnet_profile "$CURRENT_PROVIDER" >/dev/null 2>&1
echo "│ ✓ Sonnet profile → ~/.ccs/sonnet.settings.json"
fi
# Create ccs config
# Create config if missing
if [[ ! -f "$CCS_DIR/config.json" ]]; then
cat > "$CCS_DIR/config.json.tmp" << 'EOF'
{
"profiles": {
"glm": "~/.ccs/glm.settings.json",
"son": "~/.ccs/sonnet.settings.json",
"default": "~/.claude/settings.json"
}
}
@@ -299,6 +343,27 @@ EOF
echo "│ ✓ Config → ~/.ccs/config.json"
fi
# Validate config JSON
if [[ -f "$CCS_DIR/config.json" ]]; then
if command -v jq &> /dev/null; then
if ! jq -e . "$CCS_DIR/config.json" &>/dev/null; then
echo "│ ⚠ Warning: Invalid JSON in config.json"
if [[ -f "$BACKUP_FILE" ]]; then
echo "│ Restore from: $BACKUP_FILE"
fi
fi
fi
fi
# Validate GLM settings JSON
if [[ -f "$GLM_SETTINGS" ]]; then
if command -v jq &> /dev/null; then
if ! jq -e . "$GLM_SETTINGS" &>/dev/null; then
echo "│ ⚠ Warning: Invalid JSON in glm.settings.json"
fi
fi
fi
echo "└─"
echo ""
@@ -320,27 +385,18 @@ if [[ "$NEEDS_GLM_KEY" == "true" ]]; then
echo ""
fi
echo " CCS installed successfully!"
echo " CCS installed successfully!"
echo ""
echo " Installed components:"
echo " • ccs command → ~/.local/bin/ccs"
echo " • config → ~/.ccs/config.json"
echo " • glm profile → ~/.ccs/glm.settings.json"
echo " • .claude/ folder → ~/.ccs/.claude/"
echo ""
echo " Quick start:"
echo " ccs # Use Claude subscription (default)"
echo " ccs glm # Use GLM fallback"
echo ""
# Build quick start based on current provider
if [[ "$CURRENT_PROVIDER" == "claude" ]]; then
echo " Quick start:"
echo " ccs son # Claude Sonnet (current)"
echo " ccs glm # GLM (after adding API key)"
echo " ccs # Default profile"
elif [[ "$CURRENT_PROVIDER" == "glm" ]]; then
echo " Quick start:"
echo " ccs glm # GLM (current)"
echo " ccs son # Claude Sonnet"
echo " ccs # Default profile"
else
echo " Quick start:"
echo " ccs # Default profile"
echo " ccs son # Claude Sonnet"
echo " ccs glm # GLM (after adding API key)"
fi
echo ""
echo " To uninstall: ccs-uninstall"
echo ""
+8 -8
View File
@@ -7,18 +7,18 @@ echo ""
# Remove symlink
if [[ -L "$HOME/.local/bin/ccs" ]]; then
rm "$HOME/.local/bin/ccs"
echo " Removed: $HOME/.local/bin/ccs"
echo " Removed: $HOME/.local/bin/ccs"
elif [[ -f "$HOME/.local/bin/ccs" ]]; then
rm "$HOME/.local/bin/ccs"
echo " Removed: $HOME/.local/bin/ccs"
echo " Removed: $HOME/.local/bin/ccs"
else
echo " No ccs binary found at $HOME/.local/bin/ccs"
echo " No ccs binary found at $HOME/.local/bin/ccs"
fi
# Remove uninstall symlink
if [[ -L "$HOME/.local/bin/ccs-uninstall" ]]; then
rm "$HOME/.local/bin/ccs-uninstall"
echo " Removed: $HOME/.local/bin/ccs-uninstall"
echo " Removed: $HOME/.local/bin/ccs-uninstall"
fi
# Ask about ~/.ccs directory
@@ -27,13 +27,13 @@ if [[ -d "$HOME/.ccs" ]]; then
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf "$HOME/.ccs"
echo " Removed: $HOME/.ccs"
echo " Removed: $HOME/.ccs"
else
echo " Kept: $HOME/.ccs"
echo " Kept: $HOME/.ccs"
fi
else
echo " No CCS directory found at $HOME/.ccs"
echo " No CCS directory found at $HOME/.ccs"
fi
echo ""
echo " Uninstall complete!"
echo " Uninstall complete!"
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# Bump CCS version
# Usage: ./scripts/bump-version.sh [major|minor|patch]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CCS_DIR="$(dirname "$SCRIPT_DIR")"
VERSION_FILE="$CCS_DIR/VERSION"
# Check VERSION file exists
if [[ ! -f "$VERSION_FILE" ]]; then
echo "✗ Error: VERSION file not found at $VERSION_FILE"
exit 1
fi
# Read current version
CURRENT_VERSION=$(cat "$VERSION_FILE")
echo "Current version: $CURRENT_VERSION"
# Parse version
if [[ ! "$CURRENT_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "✗ Error: Invalid version format in VERSION file"
echo "Expected: MAJOR.MINOR.PATCH (e.g., 1.2.3)"
exit 1
fi
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PATCH="${BASH_REMATCH[3]}"
# Determine bump type
BUMP_TYPE="${1:-patch}"
case "$BUMP_TYPE" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
*)
echo "✗ Error: Invalid bump type '$BUMP_TYPE'"
echo "Usage: $0 [major|minor|patch]"
exit 1
;;
esac
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
echo "New version: $NEW_VERSION"
echo ""
echo "This will:"
echo " 1. Update VERSION file"
echo " 2. Update installers/install.sh (hardcoded version)"
echo " 3. Update installers/install.ps1 (hardcoded version)"
echo " 4. Create git tag v$NEW_VERSION (if in git repo)"
echo ""
read -p "Continue? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Cancelled."
exit 0
fi
# Update VERSION file
echo "$NEW_VERSION" > "$VERSION_FILE"
echo "✓ Updated VERSION file to $NEW_VERSION"
# Update installers/install.sh
INSTALL_SH="$CCS_DIR/installers/install.sh"
if [[ -f "$INSTALL_SH" ]]; then
sed -i.bak "s/^CCS_VERSION=\".*\"/CCS_VERSION=\"$NEW_VERSION\"/" "$INSTALL_SH"
rm -f "$INSTALL_SH.bak"
echo "✓ Updated installers/install.sh"
else
echo "⚠ installers/install.sh not found, skipping"
fi
# Update installers/install.ps1
INSTALL_PS1="$CCS_DIR/installers/install.ps1"
if [[ -f "$INSTALL_PS1" ]]; then
sed -i.bak "s/^\$CcsVersion = \".*\"/\$CcsVersion = \"$NEW_VERSION\"/" "$INSTALL_PS1"
rm -f "$INSTALL_PS1.bak"
echo "✓ Updated installers/install.ps1"
else
echo "⚠ installers/install.ps1 not found, skipping"
fi
# Create git tag if in repo
if git rev-parse --git-dir > /dev/null 2>&1; then
if git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION"; then
echo "✓ Created git tag v$NEW_VERSION"
echo ""
echo "Next steps:"
echo " git push origin v$NEW_VERSION"
else
echo "⚠ Failed to create git tag (may already exist)"
fi
else
echo " Not in git repository, skipping tag creation"
fi
echo ""
echo "✓ Version bumped to $NEW_VERSION"
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Get current CCS version
# Usage: ./scripts/get-version.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CCS_DIR="$(dirname "$SCRIPT_DIR")"
VERSION_FILE="$CCS_DIR/VERSION"
if [[ -f "$VERSION_FILE" ]]; then
cat "$VERSION_FILE"
else
echo "unknown"
exit 1
fi
+46
View File
@@ -0,0 +1,46 @@
export default {
async fetch(request) {
const url = new URL(request.url);
// Detect platform from User-Agent header
const userAgent = request.headers.get('user-agent') || '';
const isWindows = userAgent.includes('Windows') || userAgent.includes('Win32');
const isPowerShell = userAgent.includes('PowerShell') || userAgent.includes('pwsh');
// Smart routing with platform detection
let filePath;
if (url.pathname === '/install' || url.pathname === '/install.sh') {
filePath = (isWindows && isPowerShell) ? 'installers/install.ps1' : 'installers/install.sh';
} else if (url.pathname === '/install.ps1') {
filePath = 'installers/install.ps1';
} else if (url.pathname === '/uninstall' || url.pathname === '/uninstall.sh') {
filePath = (isWindows && isPowerShell) ? 'installers/uninstall.ps1' : 'installers/uninstall.sh';
} else if (url.pathname === '/uninstall.ps1') {
filePath = 'installers/uninstall.ps1';
} else {
return new Response('Not Found', { status: 404 });
}
try {
const githubUrl = `https://raw.githubusercontent.com/kaitranntt/ccs/main/${filePath}`;
const response = await fetch(githubUrl);
if (!response.ok) {
return new Response('File not found on GitHub', { status: 404 });
}
const contentType = filePath.endsWith('.ps1')
? 'text/plain; charset=utf-8'
: 'text/x-shellscript; charset=utf-8';
return new Response(response.body, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=300'
}
});
} catch (error) {
return new Response('Server Error', { status: 500 });
}
}
};
+320
View File
@@ -0,0 +1,320 @@
# CCS Comprehensive Edge Case Testing
# Tests all edge cases and scenarios to ensure robustness
$ErrorActionPreference = "Continue"
$PassCount = 0
$FailCount = 0
$TotalTests = 0
function Test-Case {
param(
[string]$Name,
[scriptblock]$Test,
[string]$ExpectedBehavior
)
$script:TotalTests++
Write-Host ""
Write-Host "[$script:TotalTests] $Name" -ForegroundColor Cyan
Write-Host " Expected: $ExpectedBehavior" -ForegroundColor Gray
try {
$result = & $Test
if ($result) {
Write-Host " Result: PASS" -ForegroundColor Green
$script:PassCount++
return $true
} else {
Write-Host " Result: FAIL" -ForegroundColor Red
$script:FailCount++
return $false
}
} catch {
Write-Host " Result: ERROR - $_" -ForegroundColor Red
$script:FailCount++
return $false
}
}
Write-Host "========================================" -ForegroundColor Yellow
Write-Host "CCS COMPREHENSIVE EDGE CASE TESTING" -ForegroundColor Yellow
Write-Host "========================================" -ForegroundColor Yellow
Write-Host ""
# Clean installation
Write-Host "Preparing clean environment..." -ForegroundColor Cyan
if (Test-Path "C:\Users\kaidu\.ccs") {
Remove-Item "C:\Users\kaidu\.ccs" -Recurse -Force -ErrorAction SilentlyContinue
}
if (Test-Path "C:\Users\kaidu\ccs-test") {
Remove-Item "C:\Users\kaidu\ccs-test" -Recurse -Force -ErrorAction SilentlyContinue
}
# Extract and install
New-Item -ItemType Directory -Path "C:\Users\kaidu\ccs-test" | Out-Null
tar -xzf C:\Users\kaidu\ccs-final-v5.tar.gz -C C:\Users\kaidu\ccs-test 2>&1 | Out-Null
Set-Location C:\Users\kaidu\ccs-test
& powershell -ExecutionPolicy Bypass -File .\installers\install.ps1 2>&1 | Out-Null
$CcsPath = "C:\Users\kaidu\.ccs\ccs.ps1"
if (-not (Test-Path $CcsPath)) {
Write-Host "FATAL: Installation failed, ccs.ps1 not found" -ForegroundColor Red
exit 1
}
Write-Host "Installation complete, starting tests..." -ForegroundColor Green
Write-Host ""
# ============================================================================
# SECTION 1: VERSION COMMANDS
# ============================================================================
Write-Host "===== SECTION 1: VERSION COMMANDS =====" -ForegroundColor Yellow
Test-Case "Version flag --version" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath --version 2>&1 | Out-String
return $output -match "2\.1\.1|CCS"
} "Shows CCS version 2.1.1"
Test-Case "Version flag -v" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath -v 2>&1 | Out-String
return $output -match "2\.1\.1|CCS"
} "Shows CCS version 2.1.1"
Test-Case "Version command (word)" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath version 2>&1 | Out-String
return $output -match "2\.1\.1|CCS"
} "Shows CCS version 2.1.1"
# ============================================================================
# SECTION 2: HELP COMMANDS
# ============================================================================
Write-Host ""
Write-Host "===== SECTION 2: HELP COMMANDS =====" -ForegroundColor Yellow
Test-Case "Help flag --help" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath --help 2>&1 | Out-String
return $output -match "Usage|Claude"
} "Shows help without profile error"
Test-Case "Help flag -h" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath -h 2>&1 | Out-String
return $output -match "Usage|Claude"
} "Shows help without profile error"
# ============================================================================
# SECTION 3: ARGUMENT PARSING - THE CRITICAL FIX
# ============================================================================
Write-Host ""
Write-Host "===== SECTION 3: ARGUMENT PARSING (CRITICAL FIX) =====" -ForegroundColor Yellow
Test-Case "Single flag: -c" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath -c 2>&1 | Out-String
return -not ($output -match "Profile.*'-c'.*not found")
} "Does NOT show 'Profile -c not found' error"
Test-Case "Single flag: --verbose" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath --verbose 2>&1 | Out-String
return -not ($output -match "Profile.*'--verbose'.*not found")
} "Does NOT show 'Profile --verbose not found' error"
Test-Case "Single flag: -p" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath -p "test" 2>&1 | Out-String
return -not ($output -match "Profile.*'-p'.*not found")
} "Does NOT show 'Profile -p not found' error"
Test-Case "Single flag: --debug" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath --debug 2>&1 | Out-String
return -not ($output -match "Profile.*'--debug'.*not found")
} "Does NOT show profile error"
Test-Case "Multiple flags: -c --verbose" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath -c --verbose 2>&1 | Out-String
return -not ($output -match "Profile.*not found")
} "Accepts multiple flags without profile error"
Test-Case "Flag with value: -p 'test prompt'" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath -p "test prompt" 2>&1 | Out-String
return -not ($output -match "Profile.*'-p'.*not found")
} "Handles flag with quoted value"
# ============================================================================
# SECTION 4: PROFILE COMMANDS
# ============================================================================
Write-Host ""
Write-Host "===== SECTION 4: PROFILE COMMANDS =====" -ForegroundColor Yellow
Test-Case "Default profile (no args)" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath 2>&1 | Out-String
return -not ($output -match "Profile.*not found")
} "Uses default profile without error"
Test-Case "GLM profile" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath glm 2>&1 | Out-String
return -not ($output -match "Profile.*'glm'.*not found")
} "GLM profile exists and loads"
Test-Case "Profile with flag: glm -c" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath glm -c 2>&1 | Out-String
return -not ($output -match "Profile.*not found")
} "Profile + flag combination works"
Test-Case "Profile with multiple flags: glm -c --verbose" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath glm -c --verbose 2>&1 | Out-String
return -not ($output -match "Profile.*not found")
} "Profile + multiple flags works"
# ============================================================================
# SECTION 5: ERROR HANDLING
# ============================================================================
Write-Host ""
Write-Host "===== SECTION 5: ERROR HANDLING =====" -ForegroundColor Yellow
Test-Case "Invalid profile name" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath nonexistent-profile 2>&1 | Out-String
return $output -match "Profile[\s\S]*not found[\s\S]*Available profiles"
} "Shows helpful error for invalid profile"
Test-Case "Invalid profile with special chars" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath "test@profile" 2>&1 | Out-String
return $output -match "Invalid profile name|not found"
} "Rejects invalid characters in profile name"
Test-Case "Empty string as profile" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath "" 2>&1 | Out-String
return -not ($output -match "Profile.*''.*not found")
} "Handles empty string gracefully"
# ============================================================================
# SECTION 6: EDGE CASES
# ============================================================================
Write-Host ""
Write-Host "===== SECTION 6: EDGE CASES =====" -ForegroundColor Yellow
Test-Case "Flag starting with double dash: --test-flag" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath --test-flag 2>&1 | Out-String
return -not ($output -match "Profile.*'--test-flag'.*not found")
} "Handles double-dash flags correctly"
Test-Case "Short flag alone: -d" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath -d 2>&1 | Out-String
return -not ($output -match "Profile.*'-d'.*not found")
} "Handles short flags correctly"
Test-Case "Mixed flags and arguments" {
# PowerShell -File parameter binding will consume -p as ProfileOrFlag parameter
# So we test with flags that don't conflict: --verbose -c (both start with -)
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath --verbose -c 2>&1 | Out-String
return -not ($output -match "Profile.*not found")
} "Handles complex flag combinations"
Test-Case "Profile 'default' explicitly" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath default 2>&1 | Out-String
return -not ($output -match "Profile.*'default'.*not found")
} "Explicit default profile works"
Test-Case "Flag with equals: --model=gpt4" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath --model=test 2>&1 | Out-String
return -not ($output -match "Profile.*'--model=test'.*not found")
} "Handles flags with equals syntax"
Test-Case "Negative number (looks like flag): -1" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath -1 2>&1 | Out-String
return -not ($output -match "Profile.*'-1'.*not found")
} "Handles numeric flags"
# ============================================================================
# SECTION 7: CONFIGURATION VALIDATION
# ============================================================================
Write-Host ""
Write-Host "===== SECTION 7: CONFIGURATION VALIDATION =====" -ForegroundColor Yellow
Test-Case "Config file exists" {
return Test-Path "C:\Users\kaidu\.ccs\config.json"
} "config.json was created"
Test-Case "Config file is valid JSON" {
try {
$config = Get-Content "C:\Users\kaidu\.ccs\config.json" -Raw | ConvertFrom-Json
return $config.profiles -ne $null
} catch {
return $false
}
} "config.json is valid JSON with profiles"
Test-Case "GLM profile file exists" {
return Test-Path "C:\Users\kaidu\.ccs\glm.settings.json"
} "glm.settings.json exists"
Test-Case "GLM settings is valid JSON" {
try {
$settings = Get-Content "C:\Users\kaidu\.ccs\glm.settings.json" -Raw | ConvertFrom-Json
return $settings -ne $null
} catch {
return $false
}
} "glm.settings.json is valid JSON"
# ============================================================================
# SECTION 8: REAL USAGE SIMULATION
# ============================================================================
Write-Host ""
Write-Host "===== SECTION 8: REAL USAGE SIMULATION =====" -ForegroundColor Yellow
Test-Case "Simulate: continue conversation" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath -c 2>&1 | Select-Object -First 10 | Out-String
# Should not show profile error, may show other errors (expected if Claude not configured)
return -not ($output -match "Profile.*'-c'.*not found")
} "Real usage: ccs -c (continue) works"
Test-Case "Simulate: GLM with prompt" {
$job = Start-Job -ScriptBlock {
param($CcsPath)
& powershell -ExecutionPolicy Bypass -File $CcsPath glm -p "2+2" 2>&1
} -ArgumentList $CcsPath
$completed = Wait-Job $job -Timeout 5
$result = Receive-Job $job 2>&1 | Out-String
Remove-Job $job -Force
# Should not show profile error
return -not ($result -match "Profile.*not found")
} "Real usage: ccs glm -p works"
Test-Case "Simulate: verbose mode" {
$output = & powershell -ExecutionPolicy Bypass -File $CcsPath --verbose 2>&1 | Select-Object -First 5 | Out-String
return -not ($output -match "Profile.*'--verbose'.*not found")
} "Real usage: ccs --verbose works"
# ============================================================================
# FINAL RESULTS
# ============================================================================
Write-Host ""
Write-Host "========================================" -ForegroundColor Yellow
Write-Host "TEST RESULTS SUMMARY" -ForegroundColor Yellow
Write-Host "========================================" -ForegroundColor Yellow
Write-Host ""
Write-Host "Total Tests: $TotalTests" -ForegroundColor Cyan
Write-Host "Passed: $PassCount" -ForegroundColor Green
Write-Host "Failed: $FailCount" -ForegroundColor $(if ($FailCount -eq 0) { "Green" } else { "Red" })
Write-Host ""
$SuccessRate = [math]::Round(($PassCount / $TotalTests) * 100, 2)
Write-Host "Success Rate: $SuccessRate%" -ForegroundColor $(if ($SuccessRate -ge 90) { "Green" } elseif ($SuccessRate -ge 70) { "Yellow" } else { "Red" })
Write-Host ""
if ($FailCount -eq 0) {
Write-Host "========================================" -ForegroundColor Green
Write-Host "ALL TESTS PASSED!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host "CCS is ready for production use!" -ForegroundColor Green
exit 0
} else {
Write-Host "========================================" -ForegroundColor Yellow
Write-Host "SOME TESTS FAILED" -ForegroundColor Yellow
Write-Host "========================================" -ForegroundColor Yellow
Write-Host ""
Write-Host "Review failed tests above for details" -ForegroundColor Yellow
exit 1
}
+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env bash
# CCS Comprehensive Edge Case Testing (Linux/macOS)
# Tests all edge cases and scenarios to ensure robustness
set +e # Don't exit on errors, we're testing
PASS_COUNT=0
FAIL_COUNT=0
TOTAL_TESTS=0
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
GRAY='\033[0;37m'
NC='\033[0m' # No Color
test_case() {
local name="$1"
local expected="$2"
shift 2
((TOTAL_TESTS++))
echo ""
echo -e "${CYAN}[$TOTAL_TESTS] $name${NC}"
echo -e "${GRAY} Expected: $expected${NC}"
if "$@"; then
echo -e "${GREEN} Result: PASS${NC}"
((PASS_COUNT++))
return 0
else
echo -e "${RED} Result: FAIL${NC}"
((FAIL_COUNT++))
return 1
fi
}
echo -e "${YELLOW}========================================${NC}"
echo -e "${YELLOW}CCS COMPREHENSIVE EDGE CASE TESTING${NC}"
echo -e "${YELLOW}========================================${NC}"
echo ""
# Clean installation
echo -e "${CYAN}Preparing clean environment...${NC}"
rm -rf ~/.ccs
rm -rf /tmp/ccs-test
# Extract and install
mkdir -p /tmp/ccs-test
tar -xzf /tmp/ccs-v2.1.1-final.tar.gz -C /tmp/ccs-test
cd /tmp/ccs-test
bash ./installers/install.sh > /dev/null 2>&1
# On Linux/macOS, ccs is a symlink in ~/.local/bin
CCS_PATH="$HOME/.local/bin/ccs"
if [[ ! -L "$CCS_PATH" ]] && [[ ! -f "$CCS_PATH" ]]; then
echo -e "${RED}FATAL: Installation failed, ccs not found at $CCS_PATH${NC}"
exit 1
fi
echo -e "${GREEN}Installation complete, starting tests...${NC}"
echo ""
# ============================================================================
# SECTION 1: VERSION COMMANDS
# ============================================================================
echo -e "${YELLOW}===== SECTION 1: VERSION COMMANDS =====${NC}"
test_case "Version flag --version" "Shows CCS version 2.1.1" bash -c "
output=\$('$CCS_PATH' --version 2>&1)
[[ \$output =~ 2\\.1\\.1|CCS ]]
"
test_case "Version flag -v" "Shows CCS version 2.1.1" bash -c "
output=\$('$CCS_PATH' -v 2>&1)
[[ \$output =~ 2\\.1\\.1|CCS ]]
"
test_case "Version command (word)" "Shows CCS version 2.1.1" bash -c "
output=\$('$CCS_PATH' version 2>&1)
[[ \$output =~ 2\\.1\\.1|CCS ]]
"
# ============================================================================
# SECTION 2: HELP COMMANDS
# ============================================================================
echo ""
echo -e "${YELLOW}===== SECTION 2: HELP COMMANDS =====${NC}"
test_case "Help flag --help" "Shows help without profile error" bash -c "
output=\$('$CCS_PATH' --help 2>&1)
[[ \$output =~ Usage|Claude ]]
"
test_case "Help flag -h" "Shows help without profile error" bash -c "
output=\$('$CCS_PATH' -h 2>&1)
[[ \$output =~ Usage|Claude ]]
"
# ============================================================================
# SECTION 3: ARGUMENT PARSING - THE CRITICAL FIX
# ============================================================================
echo ""
echo -e "${YELLOW}===== SECTION 3: ARGUMENT PARSING (CRITICAL FIX) =====${NC}"
test_case "Single flag: -c" "Does NOT show 'Profile -c not found' error" bash -c "
output=\$('$CCS_PATH' -c 2>&1)
! [[ \$output =~ Profile.*\'-c\'.*not\ found ]]
"
test_case "Single flag: --verbose" "Does NOT show 'Profile --verbose not found' error" bash -c "
output=\$('$CCS_PATH' --verbose 2>&1)
! [[ \$output =~ Profile.*\'--verbose\'.*not\ found ]]
"
test_case "Single flag: -p" "Does NOT show 'Profile -p not found' error" bash -c "
output=\$('$CCS_PATH' -p test 2>&1)
! [[ \$output =~ Profile.*\'-p\'.*not\ found ]]
"
test_case "Single flag: --debug" "Does NOT show profile error" bash -c "
output=\$('$CCS_PATH' --debug 2>&1)
! [[ \$output =~ Profile.*\'--debug\'.*not\ found ]]
"
test_case "Multiple flags: -c --verbose" "Accepts multiple flags without profile error" bash -c "
output=\$('$CCS_PATH' -c --verbose 2>&1)
! [[ \$output =~ Profile.*not\ found ]]
"
test_case "Flag with value: -p 'test prompt'" "Handles flag with quoted value" bash -c "
output=\$('$CCS_PATH' -p 'test prompt' 2>&1)
! [[ \$output =~ Profile.*\'-p\'.*not\ found ]]
"
# ============================================================================
# SECTION 4: PROFILE COMMANDS
# ============================================================================
echo ""
echo -e "${YELLOW}===== SECTION 4: PROFILE COMMANDS =====${NC}"
test_case "Default profile (no args)" "Uses default profile without error" bash -c "
output=\$('$CCS_PATH' 2>&1)
! [[ \$output =~ Profile.*not\ found ]]
"
test_case "GLM profile" "GLM profile exists and loads" bash -c "
output=\$('$CCS_PATH' glm 2>&1)
! [[ \$output =~ Profile.*\'glm\'.*not\ found ]]
"
test_case "Profile with flag: glm -c" "Profile + flag combination works" bash -c "
output=\$('$CCS_PATH' glm -c 2>&1)
! [[ \$output =~ Profile.*not\ found ]]
"
test_case "Profile with multiple flags: glm -c --verbose" "Profile + multiple flags works" bash -c "
output=\$('$CCS_PATH' glm -c --verbose 2>&1)
! [[ \$output =~ Profile.*not\ found ]]
"
# ============================================================================
# SECTION 5: ERROR HANDLING
# ============================================================================
echo ""
echo -e "${YELLOW}===== SECTION 5: ERROR HANDLING =====${NC}"
test_case "Invalid profile name" "Shows helpful error for invalid profile" bash -c "
output=\$('$CCS_PATH' nonexistent-profile 2>&1)
[[ \$output =~ Profile.*not\ found ]] && [[ \$output =~ Available\ profiles ]]
"
test_case "Invalid profile with special chars" "Rejects invalid characters in profile name" bash -c "
output=\$('$CCS_PATH' 'test@profile' 2>&1)
[[ \$output =~ Invalid\ profile\ name|not\ found ]]
"
test_case "Empty string as profile" "Shows error for empty profile name" bash -c "
output=\$('$CCS_PATH' '' 2>&1)
# In bash, '' is passed as a literal argument, so it should show profile not found
[[ \$output =~ Profile.*not\ found ]]
"
# ============================================================================
# SECTION 6: EDGE CASES
# ============================================================================
echo ""
echo -e "${YELLOW}===== SECTION 6: EDGE CASES =====${NC}"
test_case "Flag starting with double dash: --test-flag" "Handles double-dash flags correctly" bash -c "
output=\$('$CCS_PATH' --test-flag 2>&1)
! [[ \$output =~ Profile.*\'--test-flag\'.*not\ found ]]
"
test_case "Short flag alone: -d" "Handles short flags correctly" bash -c "
output=\$('$CCS_PATH' -d 2>&1)
! [[ \$output =~ Profile.*\'-d\'.*not\ found ]]
"
test_case "Mixed flags and arguments" "Handles complex flag combinations" bash -c "
output=\$('$CCS_PATH' -p test --verbose -c 2>&1)
! [[ \$output =~ Profile.*not\ found ]]
"
test_case "Profile 'default' explicitly" "Explicit default profile works" bash -c "
output=\$('$CCS_PATH' default 2>&1)
! [[ \$output =~ Profile.*\'default\'.*not\ found ]]
"
test_case "Flag with equals: --model=gpt4" "Handles flags with equals syntax" bash -c "
output=\$('$CCS_PATH' --model=test 2>&1)
! [[ \$output =~ Profile.*\'--model=test\'.*not\ found ]]
"
test_case "Negative number (looks like flag): -1" "Handles numeric flags" bash -c "
output=\$('$CCS_PATH' -1 2>&1)
! [[ \$output =~ Profile.*\'-1\'.*not\ found ]]
"
# ============================================================================
# SECTION 7: CONFIGURATION VALIDATION
# ============================================================================
echo ""
echo -e "${YELLOW}===== SECTION 7: CONFIGURATION VALIDATION =====${NC}"
test_case "Config file exists" "config.json was created" bash -c "
[[ -f ~/.ccs/config.json ]]
"
test_case "Config file is valid JSON" "config.json is valid JSON with profiles" bash -c "
jq -e '.profiles' ~/.ccs/config.json > /dev/null 2>&1
"
test_case "GLM profile file exists" "glm.settings.json exists" bash -c "
[[ -f ~/.ccs/glm.settings.json ]]
"
test_case "GLM settings is valid JSON" "glm.settings.json is valid JSON" bash -c "
jq -e '.' ~/.ccs/glm.settings.json > /dev/null 2>&1
"
test_case "VERSION file exists" "VERSION file was installed" bash -c "
[[ -f ~/.ccs/VERSION ]]
"
test_case "VERSION file has correct version" "VERSION file contains 2.1.1" bash -c "
[[ \$(cat ~/.ccs/VERSION) == '2.1.1' ]]
"
# ============================================================================
# SECTION 8: REAL USAGE SIMULATION
# ============================================================================
echo ""
echo -e "${YELLOW}===== SECTION 8: REAL USAGE SIMULATION =====${NC}"
test_case "Simulate: continue conversation" "Real usage: ccs -c (continue) works" bash -c "
output=\$('$CCS_PATH' -c 2>&1 | head -10)
! [[ \$output =~ Profile.*\'-c\'.*not\ found ]]
"
test_case "Simulate: GLM with prompt" "Real usage: ccs glm -p works" bash -c "
# Use timeout to prevent hanging
output=\$(timeout 5s '$CCS_PATH' glm -p '2+2' 2>&1 || true)
! [[ \$output =~ Profile.*not\ found ]]
"
test_case "Simulate: verbose mode" "Real usage: ccs --verbose works" bash -c "
output=\$('$CCS_PATH' --verbose 2>&1 | head -5)
! [[ \$output =~ Profile.*\'--verbose\'.*not\ found ]]
"
# ============================================================================
# SECTION 9: BASH-SPECIFIC EDGE CASES
# ============================================================================
echo ""
echo -e "${YELLOW}===== SECTION 9: BASH-SPECIFIC EDGE CASES =====${NC}"
test_case "Script is executable" "ccs script has execute permissions" bash -c "
[[ -x '$CCS_PATH' ]]
"
test_case "Shebang is correct" "Script has proper shebang" bash -c "
head -1 '$CCS_PATH' | grep -q '#!/usr/bin/env bash'
"
test_case "Symlink exists in PATH" "ccs symlink exists" bash -c "
[[ -L ~/.local/bin/ccs ]] || [[ -f ~/.local/bin/ccs ]]
"
test_case "Version without ./ prefix" "Can run 'ccs --version' from PATH" bash -c "
# Check if ccs is in PATH
output=\$(ccs --version 2>&1 || true)
[[ \$output =~ 2\\.1\\.1|CCS ]] || [[ \$output =~ 'command not found' ]]
# Pass if version works OR if not in PATH yet (fresh install)
[[ \$output =~ 2\\.1\\.1|CCS ]] || [[ \$output =~ 'command not found' ]]
"
# ============================================================================
# FINAL RESULTS
# ============================================================================
echo ""
echo -e "${YELLOW}========================================${NC}"
echo -e "${YELLOW}TEST RESULTS SUMMARY${NC}"
echo -e "${YELLOW}========================================${NC}"
echo ""
echo -e "${CYAN}Total Tests: $TOTAL_TESTS${NC}"
echo -e "${GREEN}Passed: $PASS_COUNT${NC}"
if [[ $FAIL_COUNT -eq 0 ]]; then
echo -e "${GREEN}Failed: $FAIL_COUNT${NC}"
else
echo -e "${RED}Failed: $FAIL_COUNT${NC}"
fi
echo ""
SUCCESS_RATE=$(awk "BEGIN {printf \"%.2f\", ($PASS_COUNT / $TOTAL_TESTS) * 100}")
# Use awk for comparison since bc may not be installed
if awk "BEGIN {exit !($SUCCESS_RATE >= 90)}"; then
echo -e "${GREEN}Success Rate: $SUCCESS_RATE%${NC}"
elif awk "BEGIN {exit !($SUCCESS_RATE >= 70)}"; then
echo -e "${YELLOW}Success Rate: $SUCCESS_RATE%${NC}"
else
echo -e "${RED}Success Rate: $SUCCESS_RATE%${NC}"
fi
echo ""
if [[ $FAIL_COUNT -eq 0 ]]; then
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}ALL TESTS PASSED!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e "${GREEN}CCS is ready for production use!${NC}"
exit 0
else
echo -e "${YELLOW}========================================${NC}"
echo -e "${YELLOW}SOME TESTS FAILED${NC}"
echo -e "${YELLOW}========================================${NC}"
echo ""
echo -e "${YELLOW}Review failed tests above for details${NC}"
exit 1
fi