mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
feat(shared): implement shared data architecture (v3.1.0)
Phase 1: Multi-profile shared data via symlinks Added: - SharedManager class for symlink orchestration (bin/shared-manager.js) - Auto-migration from ~/.claude/ to ~/.ccs/shared/ on first run - Shared directories: commands/, skills/, agents/ - Windows fallback: copies dirs if symlinks fail Fixed: - Migration logic now detects empty directories - Previously skipped migration when postinstall created empty dirs - Now properly copies from ~/.claude/ when shared dirs are empty Changed: - Instance initialization symlinks to shared dirs instead of copying - Postinstall creates ~/.ccs/shared/ structure automatically - All implementations (Node.js, bash, PowerShell) updated for consistency - Help text includes agents/ in shared data section Technical: - Profile-specific data remains isolated (settings, sessions, todolists, logs) - Migration is idempotent: safe to run multiple times - Cross-platform symlink support with graceful fallback Closes #4
This commit is contained in:
1 parent
df549ddf4b
commit
d925dfc169
15 files changed
+929
-708
No files matched your search
@@ -4,6 +4,33 @@ All notable changes to CCS will be documented here.
|
||||
|
||||
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [3.1.0] - 2025-11-10
|
||||
|
||||
### Added
|
||||
- **Shared Data Architecture** (Phase 1): Commands, skills, and agents now shared across all profiles
|
||||
- Single source: `~/.ccs/shared/{commands,skills,agents}` symlinked to all instances
|
||||
- Eliminates duplication across profile instances
|
||||
- Profile-specific data remains isolated (settings, sessions, todolists, logs)
|
||||
- Auto-migration from `~/.claude/` to `~/.ccs/shared/` on first run
|
||||
- Windows fallback: copies directories if symlinks fail (enable Developer Mode for native symlinks)
|
||||
|
||||
### Fixed
|
||||
- **Migration Logic**: Fixed bug where migration check only verified directory existence
|
||||
- Migration now detects empty directories (postinstall creates empty dirs, causing skip)
|
||||
- Properly copies from `~/.claude/` when shared directories are empty
|
||||
- Idempotent: safe to run multiple times, only migrates when needed
|
||||
|
||||
### Changed
|
||||
- Instance initialization now symlinks to shared directories instead of copying
|
||||
- Postinstall creates `~/.ccs/shared/` structure automatically
|
||||
- All three implementations (Node.js, bash, PowerShell) updated for consistency
|
||||
|
||||
### Technical Details
|
||||
- **New Files**: `bin/shared-manager.js` - SharedManager class for symlink orchestration
|
||||
- **Modified Files**: `bin/ccs.js`, `lib/ccs`, `lib/ccs.ps1`, `scripts/postinstall.js`
|
||||
- **Migration**: Runs automatically during first `ccs` execution after install
|
||||
- **Cross-Platform**: Symlink support with graceful Windows fallback
|
||||
|
||||
## [3.0.2] - 2025-11-10
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,257 +1,250 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
Guidance for Claude Code when working with this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
CCS (Claude Code Switch) is a lightweight CLI wrapper enabling instant switching between **multiple Claude subscription accounts** (work, personal, team) and alternative models (GLM 4.6, Kimi for Coding). Built on v3.0 login-per-profile architecture where each profile is an isolated Claude instance.
|
||||
CCS (Claude Code Switch): CLI wrapper for instant switching between multiple Claude accounts (work, personal, team) and alternative models (GLM 4.6, Kimi). Built on v3.1 login-per-profile architecture with shared data support.
|
||||
|
||||
**Primary Installation Methods** (highest priority):
|
||||
- **npm Package** (recommended): `npm install -g @kaitranntt/ccs` (cross-platform)
|
||||
**Installation**:
|
||||
- npm: `npm install -g @kaitranntt/ccs` (recommended)
|
||||
- macOS/Linux: `curl -fsSL ccs.kaitran.ca/install | bash`
|
||||
- Windows: `irm ccs.kaitran.ca/install | iex`
|
||||
|
||||
## Core Design Principles
|
||||
|
||||
**YAGNI** (You Aren't Gonna Need It): No features "just in case"
|
||||
**KISS** (Keep It Simple): Simple bash/PowerShell/Node.js, no complexity
|
||||
**DRY** (Don't Repeat Yourself): One source of truth (config.json)
|
||||
**CLI-First UX**: The primary user experience is through the command-line interface
|
||||
- **YAGNI**: No features "just in case"
|
||||
- **KISS**: Simple bash/PowerShell/Node.js, no complexity
|
||||
- **DRY**: One source of truth (config.json)
|
||||
- **CLI-First UX**: Command-line is primary interface
|
||||
|
||||
The tool does ONE thing: enable instant switching between Claude accounts and alternative models. Never add features that violate these principles.
|
||||
Tool does ONE thing: instant switching between Claude accounts and alternative models.
|
||||
|
||||
### CLI Documentation Requirement (CRITICAL)
|
||||
### CLI Documentation (CRITICAL)
|
||||
|
||||
**All functionality changes MUST update `--help` output across all implementations:**
|
||||
- `bin/ccs.js` - handleHelpCommand() function
|
||||
- `lib/ccs` - show_help() function (bash)
|
||||
- `lib/ccs.ps1` - Show-Help function (PowerShell)
|
||||
**All functionality changes MUST update `--help` in ALL implementations:**
|
||||
- `bin/ccs.js` - handleHelpCommand()
|
||||
- `lib/ccs` - show_help()
|
||||
- `lib/ccs.ps1` - Show-Help
|
||||
|
||||
This is non-negotiable. The CLI help is the primary documentation users reference. If a feature exists but isn't documented in `--help`, it effectively doesn't exist for users.
|
||||
Non-negotiable. If not in `--help`, it doesn't exist for users.
|
||||
|
||||
## Key Constraints
|
||||
|
||||
1. **NO EMOJIS in terminal output** - Use ASCII symbols ([OK], [!], [X], [i]) for compatibility
|
||||
2. **TTY-aware color output** - Colors only when output to terminal, respects NO_COLOR env var
|
||||
3. **Unified install location** (v2.2.0+):
|
||||
- All Unix: `~/.local/bin` (auto PATH config, no sudo)
|
||||
1. **NO EMOJIS** - ASCII only: [OK], [!], [X], [i]
|
||||
2. **TTY-aware colors** - Respect NO_COLOR env var
|
||||
3. **Install locations**:
|
||||
- Unix: `~/.local/bin` (auto PATH, no sudo)
|
||||
- Windows: `%USERPROFILE%\.ccs`
|
||||
4. **Auto PATH configuration** - Detects shell (bash/zsh/fish), adds to profile automatically
|
||||
5. **Idempotent installations** - Running install scripts multiple times must be safe
|
||||
4. **Auto PATH config** - Detects bash/zsh/fish, adds automatically
|
||||
5. **Idempotent installs** - Safe to run multiple times
|
||||
6. **Non-invasive** - Never modify `~/.claude/settings.json`
|
||||
7. **Cross-platform parity** - Identical behavior on Unix/Linux/macOS/Windows
|
||||
8. **Edge case handling** - Handle all scenarios gracefully (see tests/edge-cases.sh)
|
||||
7. **Cross-platform parity** - Identical behavior everywhere
|
||||
8. **Graceful error handling** - See tests/edge-cases.sh
|
||||
|
||||
## Architecture
|
||||
|
||||
### v3.0 Key Features
|
||||
### v3.1 Features
|
||||
|
||||
**Login-Per-Profile Model**: Each profile is an isolated Claude instance where users login directly via `ccs auth create <profile>`. No credential copying, no vault encryption.
|
||||
**Login-Per-Profile**: Each profile = isolated Claude instance via `ccs auth create <profile>`. No credential copying/encryption.
|
||||
|
||||
**Two Profile Types**:
|
||||
1. **Settings-based** (models): GLM, Kimi, default - uses `--settings` flag
|
||||
2. **Account-based** (Claude accounts): work, personal, team - uses `CLAUDE_CONFIG_DIR`
|
||||
**Profile Types**:
|
||||
1. **Settings-based**: GLM, Kimi, default - uses `--settings` flag
|
||||
2. **Account-based**: work, personal, team - uses `CLAUDE_CONFIG_DIR`
|
||||
|
||||
**Concurrent Sessions**: Multiple profiles can run simultaneously in different terminals via isolated config directories.
|
||||
**Shared Data** (v3.1): commands/, skills/, agents/ symlinked from `~/.ccs/shared/`
|
||||
|
||||
### Implementation Variants
|
||||
**Concurrent Sessions**: Multiple profiles run simultaneously via isolated config dirs.
|
||||
|
||||
**npm package**: Pure Node.js (bin/ccs.js) using child_process.spawn
|
||||
**Traditional install**: Platform-specific bash (lib/ccs) or PowerShell (lib/ccs.ps1)
|
||||
**Implementations**:
|
||||
- npm package: Pure Node.js (bin/ccs.js) using child_process.spawn
|
||||
- Traditional: bash (lib/ccs) or PowerShell (lib/ccs.ps1)
|
||||
|
||||
### File Structure
|
||||
|
||||
**Key Files**:
|
||||
- `package.json`: npm package manifest with bin field configuration and postinstall script
|
||||
- `bin/ccs.js`: Cross-platform Node.js entry point (npm package)
|
||||
- `scripts/postinstall.js`: Auto-creates config files during npm install (idempotent)
|
||||
- `lib/ccs` (bash) / `lib/ccs.ps1` (PowerShell): Platform-specific executable wrappers
|
||||
- `installers/install.sh` / `installers/install.ps1`: Traditional installation scripts
|
||||
- `installers/uninstall.sh` / `installers/uninstall.ps1`: Removal scripts
|
||||
- `VERSION`: Single source of truth for version (format: MAJOR.MINOR.PATCH)
|
||||
- `.claude/`: Commands and skills for Claude Code integration
|
||||
- `package.json`: npm manifest + postinstall
|
||||
- `bin/ccs.js`: Node.js entry point
|
||||
- `bin/instance-manager.js`: Instance orchestration
|
||||
- `bin/shared-manager.js`: Shared data symlinks (v3.1)
|
||||
- `scripts/postinstall.js`: Auto-creates configs (idempotent)
|
||||
- `lib/ccs` / `lib/ccs.ps1`: Platform-specific executables
|
||||
- `installers/*.sh` / `installers/*.ps1`: Install/uninstall scripts
|
||||
- `VERSION`: Version source of truth (MAJOR.MINOR.PATCH)
|
||||
- `.claude/`: Commands/skills for Claude Code
|
||||
|
||||
**Executable Locations**:
|
||||
- macOS / Linux: `~/.local/bin/ccs` (symlink to `~/.ccs/ccs`)
|
||||
**Executables**:
|
||||
- Unix: `~/.local/bin/ccs` → `~/.ccs/ccs`
|
||||
- Windows: `%USERPROFILE%\.ccs\ccs.ps1`
|
||||
|
||||
**Configuration Directory**:
|
||||
**Config Directory** (v3.1):
|
||||
```
|
||||
~/.ccs/
|
||||
├── ccs # Main executable (or ccs.ps1 on Windows)
|
||||
├── config.json # Settings-based profile mappings
|
||||
├── profiles.json # Account-based profile registry (v3.0)
|
||||
├── instances/ # Isolated Claude instances (v3.0)
|
||||
│ ├── work/ # Each profile gets own directory
|
||||
│ ├── personal/
|
||||
│ └── team/
|
||||
├── config.json.backup # Single backup (overwrites on each install)
|
||||
├── glm.settings.json # GLM profile template
|
||||
├── kimi.settings.json # Kimi profile template
|
||||
├── VERSION # Version file copy
|
||||
├── uninstall.sh # Uninstaller (or ccs-uninstall.ps1 on Windows)
|
||||
└── .claude/ # Claude Code integration
|
||||
├── commands/ccs.md
|
||||
└── skills/ccs-delegation/
|
||||
├── ccs / ccs.ps1 # Executable
|
||||
├── config.json # Settings-based profiles
|
||||
├── profiles.json # Account-based profiles
|
||||
├── shared/ # Shared across all (v3.1)
|
||||
│ ├── commands/ # Slash commands
|
||||
│ ├── skills/ # Claude skills
|
||||
│ └── agents/ # Agent configs
|
||||
├── instances/ # Isolated per-profile
|
||||
│ └── work/
|
||||
│ ├── commands@ → shared/commands/ # Symlink
|
||||
│ ├── skills@ → shared/skills/ # Symlink
|
||||
│ ├── agents@ → shared/agents/ # Symlink
|
||||
│ ├── settings.json # Profile-specific
|
||||
│ ├── sessions/ # Profile-specific
|
||||
│ ├── todolists/ # Profile-specific
|
||||
│ └── logs/ # Profile-specific
|
||||
├── glm.settings.json # GLM template
|
||||
├── kimi.settings.json # Kimi template
|
||||
└── .claude/ # Integration
|
||||
```
|
||||
|
||||
### v3.0 Technical Implementation
|
||||
**v3.1 Data Structure**:
|
||||
- **Shared**: commands/, skills/, agents/ → symlinked from `~/.ccs/shared/`
|
||||
- **Profile-specific**: settings.json, sessions/, todolists/, logs/
|
||||
- **Windows**: Copies dirs if symlinks fail (enable Developer Mode for symlinks)
|
||||
|
||||
**Account-Based Profiles** (v3.0):
|
||||
### Technical Implementation
|
||||
|
||||
**Account-Based Profiles**:
|
||||
```bash
|
||||
# Create profile (opens Claude CLI for login)
|
||||
ccs auth create work
|
||||
# Create profile
|
||||
ccs auth create work # Opens Claude CLI for login
|
||||
|
||||
# Creates ~/.ccs/instances/work/ with:
|
||||
# - settings.json (Claude's config)
|
||||
# - session files
|
||||
# - todo lists
|
||||
# - logs
|
||||
|
||||
# Usage: Set CLAUDE_CONFIG_DIR before spawning Claude CLI
|
||||
# Usage
|
||||
CLAUDE_CONFIG_DIR=~/.ccs/instances/work claude [args]
|
||||
```
|
||||
|
||||
**Settings-Based Profiles** (legacy, still supported):
|
||||
**Shared Data** (v3.1):
|
||||
```javascript
|
||||
// bin/shared-manager.js
|
||||
ensureSharedDirectories() // Creates shared/{commands,skills,agents}
|
||||
linkSharedDirectories(path) // Symlinks instance to shared
|
||||
migrateToSharedStructure() // Auto-migrates v3.0 (idempotent)
|
||||
```
|
||||
|
||||
**Migration** (v3.0 → v3.1):
|
||||
1. Detect if `~/.ccs/shared/` exists (skip if present)
|
||||
2. Create `~/.ccs/shared/{commands,skills,agents}`
|
||||
3. Copy from `~/.claude/` (preserves data)
|
||||
4. Symlink all instances to shared
|
||||
5. Windows: Copy dirs if symlinks fail
|
||||
|
||||
**Settings-Based Profiles**:
|
||||
```bash
|
||||
# Usage: Pass --settings flag to Claude CLI
|
||||
claude --settings ~/.ccs/glm.settings.json [args]
|
||||
```
|
||||
|
||||
**Profile Detection Logic**:
|
||||
1. Check if profile exists in `profiles.json` (account-based)
|
||||
2. If yes → use `CLAUDE_CONFIG_DIR` method
|
||||
3. If no → check `config.json` (settings-based)
|
||||
4. If yes → use `--settings` method
|
||||
5. If no → show error with available profiles
|
||||
**Profile Detection**:
|
||||
1. Check `profiles.json` (account-based) → use `CLAUDE_CONFIG_DIR`
|
||||
2. Check `config.json` (settings-based) → use `--settings`
|
||||
3. Not found → show error + available profiles
|
||||
|
||||
## Development Commands
|
||||
## Development
|
||||
|
||||
### Version Management
|
||||
```bash
|
||||
# Bump version (updates VERSION, install.sh, install.ps1)
|
||||
./scripts/bump-version.sh [major|minor|patch]
|
||||
|
||||
# Get current version
|
||||
cat VERSION
|
||||
# or
|
||||
./scripts/get-version.sh
|
||||
./scripts/bump-version.sh [major|minor|patch] # Updates VERSION, install scripts
|
||||
cat VERSION # Check version
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Comprehensive edge case testing (Unix)
|
||||
./tests/edge-cases.sh
|
||||
|
||||
# Comprehensive edge case testing (Windows)
|
||||
./tests/edge-cases.ps1
|
||||
./tests/edge-cases.sh # Unix
|
||||
./tests/edge-cases.ps1 # Windows
|
||||
```
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
# Test local installation from git repo
|
||||
./installers/install.sh
|
||||
./installers/install.sh # Test local install
|
||||
./ccs --version # Verify
|
||||
|
||||
# Test with local executable
|
||||
./ccs --version
|
||||
./ccs glm --help
|
||||
# Test npm package
|
||||
npm pack && npm install -g @kaitranntt-ccs-*.tgz
|
||||
ccs --version
|
||||
npm uninstall -g @kaitranntt/ccs && rm *.tgz
|
||||
|
||||
# Test npm package locally
|
||||
npm pack # Creates @kaitranntt-ccs-X.Y.Z.tgz
|
||||
npm install -g @kaitranntt-ccs-X.Y.Z.tgz # Test installation
|
||||
ccs --version # Verify it works
|
||||
npm uninstall -g @kaitranntt/ccs # Cleanup
|
||||
rm @kaitranntt-ccs-X.Y.Z.tgz # Remove tarball
|
||||
|
||||
# Clean test environment
|
||||
rm -rf ~/.ccs
|
||||
rm -rf ~/.ccs # Clean environment
|
||||
```
|
||||
|
||||
### npm Package Publishing
|
||||
### Publishing
|
||||
```bash
|
||||
# First-time setup (one-time)
|
||||
npm login # Login to npm account
|
||||
npm token create --type=granular --scope=publish # Create token
|
||||
# Add NPM_TOKEN to GitHub Secrets
|
||||
# First-time: npm login, add NPM_TOKEN to GitHub Secrets
|
||||
|
||||
# Publishing workflow
|
||||
./scripts/bump-version.sh patch # Bump version
|
||||
git add VERSION package.json lib/ccs lib/ccs.ps1 installers/install.sh installers/install.ps1
|
||||
# Release workflow
|
||||
./scripts/bump-version.sh patch
|
||||
git add VERSION package.json lib/* installers/*
|
||||
git commit -m "chore: bump version to X.Y.Z"
|
||||
git tag vX.Y.Z
|
||||
git push origin main
|
||||
git push origin vX.Y.Z # Triggers GitHub Actions publish
|
||||
git push origin main && git push origin vX.Y.Z # Triggers CI
|
||||
|
||||
# Manual publish (if needed)
|
||||
npm publish --dry-run # Test before publishing
|
||||
npm publish --access public # Publish to npm registry
|
||||
# Manual
|
||||
npm publish --dry-run && npm publish --access public
|
||||
```
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Bash (Unix Systems)
|
||||
- Compatibility: bash 3.2+ (macOS default)
|
||||
- Always quote variables: `"$VAR"` not `$VAR`
|
||||
- Use `[[ ]]` for tests, not `[ ]`
|
||||
- Use `#!/usr/bin/env bash` shebang
|
||||
- Set `set -euo pipefail` for safety
|
||||
- Dependencies: Only `jq` for JSON parsing
|
||||
### Bash
|
||||
- Compatibility: bash 3.2+
|
||||
- Quote vars: `"$VAR"` not `$VAR`
|
||||
- Tests: `[[ ]]` not `[ ]`
|
||||
- Shebang: `#!/usr/bin/env bash`
|
||||
- Safety: `set -euo pipefail`
|
||||
- Dependency: `jq` only
|
||||
|
||||
### Terminal Output
|
||||
- **TTY Detection**: Check `[[ -t 2 ]]` before using colors (stderr)
|
||||
- **NO_COLOR Support**: Respect `${NO_COLOR:-}` environment variable
|
||||
- **ASCII Symbols Only**: [OK], [!], [X], [i] - no emojis
|
||||
- **Error Formatting**: Use box borders (╔═╗║╚╝) for critical messages
|
||||
- **Color Codes**: RED, YELLOW, GREEN, CYAN, BOLD, RESET - disable when not TTY
|
||||
- TTY detect: `[[ -t 2 ]]` before colors
|
||||
- Respect `NO_COLOR` env var
|
||||
- ASCII only: [OK], [!], [X], [i]
|
||||
- Errors: Box borders (╔═╗║╚╝)
|
||||
- Colors: Disable when not TTY
|
||||
|
||||
### PowerShell (Windows)
|
||||
### PowerShell
|
||||
- Compatibility: PowerShell 5.1+
|
||||
- Use `$ErrorActionPreference = "Stop"`
|
||||
- Native JSON parsing via `ConvertFrom-Json` / `ConvertTo-Json`
|
||||
- No external dependencies required
|
||||
- `$ErrorActionPreference = "Stop"`
|
||||
- Native JSON: ConvertFrom-Json / ConvertTo-Json
|
||||
- No external dependencies
|
||||
|
||||
### Node.js (npm package)
|
||||
### Node.js
|
||||
- Compatibility: Node.js 14+
|
||||
- Use `child_process.spawn` for Claude CLI execution
|
||||
- Handle SIGINT/SIGTERM for graceful shutdown
|
||||
- Cross-platform path handling with `path` module
|
||||
- `child_process.spawn` for Claude CLI
|
||||
- Handle SIGINT/SIGTERM
|
||||
- `path` module for cross-platform paths
|
||||
|
||||
### Version Synchronization
|
||||
When changing version, update ALL three locations:
|
||||
1. `VERSION` file
|
||||
2. `installers/install.sh` (CCS_VERSION variable)
|
||||
3. `installers/install.ps1` ($CcsVersion variable)
|
||||
### Versioning
|
||||
Update all three atomically via `./scripts/bump-version.sh`:
|
||||
1. `VERSION`
|
||||
2. `installers/install.sh` (CCS_VERSION)
|
||||
3. `installers/install.ps1` ($CcsVersion)
|
||||
|
||||
Use `./scripts/bump-version.sh` to update all locations atomically.
|
||||
## Implementation Details
|
||||
|
||||
## Critical Implementation Details
|
||||
|
||||
### Profile Detection Logic
|
||||
The `ccs` wrapper uses smart detection:
|
||||
- No args OR first arg starts with `-` → use default profile
|
||||
- First arg doesn't start with `-` → treat as profile name
|
||||
- Special flags handled BEFORE profile detection: `--version`, `-v`, `--help`, `-h`
|
||||
- `ccs auth create <profile>` → create new account-based profile
|
||||
### Profile Detection
|
||||
- No args OR first arg starts with `-` → default profile
|
||||
- First arg no `-` → profile name
|
||||
- Special flags first: `--version`, `-v`, `--help`, `-h`
|
||||
- `ccs auth create <profile>` → create account-based profile
|
||||
|
||||
### Installation Modes
|
||||
- **Git mode**: Running from cloned repository (symlinks executables)
|
||||
- **Standalone mode**: Running via curl/irm (downloads from GitHub)
|
||||
- **Git**: Cloned repo (symlinks executables)
|
||||
- **Standalone**: curl/irm (downloads from GitHub)
|
||||
- Detection: Check if `ccs` exists in script dir/parent
|
||||
|
||||
Detection: Check if `ccs` executable exists in script directory or parent.
|
||||
### Idempotency
|
||||
Install scripts safe to run multiple times:
|
||||
- Check existing files before create
|
||||
- Single backup: `config.json.backup` (no timestamps)
|
||||
- Skip existing `.claude/` install
|
||||
- Handle clean + existing installs
|
||||
|
||||
### Idempotency Requirements
|
||||
Install scripts must be safe to run multiple times:
|
||||
- Check existing files before creating
|
||||
- Use single backup file (no timestamps): `config.json.backup`
|
||||
- Skip existing `.claude/` folder installation
|
||||
- Handle both clean and existing installations
|
||||
|
||||
### Settings File Format
|
||||
### Settings Format
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
|
||||
"ANTHROPIC_AUTH_TOKEN": "your_api_key",
|
||||
"ANTHROPIC_AUTH_TOKEN": "key",
|
||||
"ANTHROPIC_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
|
||||
@@ -259,90 +252,76 @@ Install scripts must be safe to run multiple times:
|
||||
}
|
||||
}
|
||||
```
|
||||
All values = strings (not booleans/objects) to prevent PowerShell crashes.
|
||||
|
||||
All values must be strings (not booleans/objects) to prevent PowerShell crashes.
|
||||
### Profile Files
|
||||
|
||||
### v3.0 Profile Files
|
||||
|
||||
**profiles.json** (account-based profiles):
|
||||
**profiles.json** (account-based):
|
||||
```json
|
||||
{
|
||||
"profiles": {
|
||||
"work": "~/.ccs/instances/work",
|
||||
"personal": "~/.ccs/instances/personal",
|
||||
"team": "~/.ccs/instances/team"
|
||||
}
|
||||
}
|
||||
{"profiles": {"work": "~/.ccs/instances/work"}}
|
||||
```
|
||||
|
||||
**config.json** (settings-based profiles):
|
||||
**config.json** (settings-based):
|
||||
```json
|
||||
{
|
||||
"profiles": {
|
||||
"glm": "~/.ccs/glm.settings.json",
|
||||
"kimi": "~/.ccs/kimi.settings.json",
|
||||
"default": "~/.claude/settings.json"
|
||||
}
|
||||
}
|
||||
{"profiles": {"glm": "~/.ccs/glm.settings.json"}}
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Feature
|
||||
1. Verify it aligns with YAGNI/KISS/DRY principles
|
||||
2. Implement for both bash/PowerShell and Node.js if applicable
|
||||
3. **Update `--help` output in ALL three implementations** (bin/ccs.js, lib/ccs, lib/ccs.ps1) - REQUIRED
|
||||
4. Test on all platforms (macOS, Linux, Windows)
|
||||
5. Update tests in `tests/edge-cases.sh` and `tests/edge-cases.ps1`
|
||||
6. Update CONTRIBUTING.md if it affects contributors
|
||||
7. Update README.md examples if user-facing
|
||||
### New Feature
|
||||
1. Verify YAGNI/KISS/DRY alignment
|
||||
2. Implement for bash/PowerShell/Node.js
|
||||
3. **Update `--help` in ALL three** (bin/ccs.js, lib/ccs, lib/ccs.ps1) - REQUIRED
|
||||
4. Test on macOS/Linux/Windows
|
||||
5. Update tests/edge-cases.*
|
||||
6. Update CONTRIBUTING.md if needed
|
||||
7. Update README.md if user-facing
|
||||
|
||||
### Fixing Bugs
|
||||
1. Add test case reproducing the bug
|
||||
2. Fix in both bash/PowerShell and Node.js versions
|
||||
3. Verify fix doesn't break existing tests
|
||||
4. Test on all supported platforms
|
||||
### Bug Fix
|
||||
1. Add test case reproducing bug
|
||||
2. Fix in bash/PowerShell/Node.js
|
||||
3. Verify no regression
|
||||
4. Test all platforms
|
||||
|
||||
### Releasing New Version
|
||||
1. Run `./scripts/bump-version.sh [major|minor|patch]`
|
||||
2. Review changes to VERSION, install.sh, install.ps1
|
||||
3. Test installation from both git and standalone modes
|
||||
4. Run full edge case test suite
|
||||
5. Commit and tag: `git tag v<VERSION>`
|
||||
6. Push to trigger GitHub Actions: `git push origin main && git push origin v<VERSION>`
|
||||
### Release
|
||||
1. `./scripts/bump-version.sh [major|minor|patch]`
|
||||
2. Review VERSION, install scripts
|
||||
3. Test git + standalone modes
|
||||
4. Run full test suite
|
||||
5. `git tag v<VERSION> && git push origin main && git push origin v<VERSION>`
|
||||
|
||||
## Testing Requirements
|
||||
## Testing Checklist
|
||||
|
||||
Before any PR, verify:
|
||||
- [ ] Works on macOS (bash)
|
||||
- [ ] Works on Linux (bash)
|
||||
- [ ] Works on Windows (PowerShell)
|
||||
- [ ] Works on Windows (Git Bash)
|
||||
- [ ] Handles all edge cases in test suite
|
||||
- [ ] Installation is idempotent
|
||||
- [ ] No emojis in terminal output (ASCII symbols only)
|
||||
- [ ] Version displayed correctly with install location
|
||||
- [ ] Colors work on TTY, disabled when piped
|
||||
- [ ] NO_COLOR environment variable respected
|
||||
- [ ] Auto PATH config works for bash, zsh, fish
|
||||
- [ ] Shell reload instructions shown correctly
|
||||
- [ ] PATH not duplicated on multiple installs
|
||||
- [ ] Manual PATH setup instructions clear if auto fails
|
||||
- [ ] v3.0 concurrent sessions work correctly
|
||||
- [ ] Instance isolation works (no cross-profile contamination)
|
||||
- [ ] **`--help` output updated in bin/ccs.js, lib/ccs, and lib/ccs.ps1** (if feature added/changed)
|
||||
- [ ] **`--help` output consistent across all three implementations**
|
||||
Before PR:
|
||||
- [ ] macOS (bash)
|
||||
- [ ] Linux (bash)
|
||||
- [ ] Windows (PowerShell)
|
||||
- [ ] Windows (Git Bash)
|
||||
- [ ] Edge cases pass
|
||||
- [ ] Idempotent install
|
||||
- [ ] ASCII only (no emojis)
|
||||
- [ ] Version + install location correct
|
||||
- [ ] TTY colors, disabled when piped
|
||||
- [ ] NO_COLOR respected
|
||||
- [ ] Auto PATH (bash/zsh/fish)
|
||||
- [ ] Shell reload instructions shown
|
||||
- [ ] No PATH duplication
|
||||
- [ ] Manual PATH instructions clear
|
||||
- [ ] Concurrent sessions work
|
||||
- [ ] Instance isolation (no contamination)
|
||||
- [ ] `--help` updated in bin/ccs.js, lib/ccs, lib/ccs.ps1 (if feature changed)
|
||||
- [ ] `--help` consistent across all three
|
||||
|
||||
## Integration with Claude Code
|
||||
## Claude Code Integration
|
||||
|
||||
The `.claude/` folder contains:
|
||||
- `/ccs` command: Meta-command for delegating tasks to different models
|
||||
- `ccs-delegation` skill: Intelligent task delegation patterns
|
||||
`.claude/` contains:
|
||||
- `/ccs` command: Task delegation to different models
|
||||
- `ccs-delegation` skill: Delegation patterns
|
||||
|
||||
## Error Handling Philosophy
|
||||
## Error Handling
|
||||
|
||||
- Validate early, fail fast with clear error messages
|
||||
- Show available options when user makes mistake
|
||||
- Suggest recovery steps (e.g., restore from backup)
|
||||
- Never leave system in broken state
|
||||
- For v3.0 profiles: Guide users through `ccs auth create` if profile missing
|
||||
- Validate early, fail fast with clear messages
|
||||
- Show available options on mistakes
|
||||
- Suggest recovery steps
|
||||
- Never leave broken state
|
||||
- Guide to `ccs auth create` if profile missing
|
||||
@@ -92,6 +92,16 @@ $env:CCS_CLAUDE_PATH = "D:\Tools\Claude\claude.exe" # Windows
|
||||
|
||||
**See [Troubleshooting Guide](./docs/en/troubleshooting.md#claude-cli-in-non-standard-location) for detailed setup instructions.**
|
||||
|
||||
### Windows Symlink Support (Developer Mode)
|
||||
|
||||
**Windows users**: Enable Developer Mode for true symlinks (better performance, instant sync):
|
||||
|
||||
1. Open **Settings** → **Privacy & Security** → **For developers**
|
||||
2. Enable **Developer Mode**
|
||||
3. Reinstall CCS: `npm install -g @kaitranntt/ccs`
|
||||
|
||||
**Without Developer Mode**: CCS automatically falls back to copying directories (works but no instant sync across profiles).
|
||||
|
||||
---
|
||||
|
||||
### Your First Switch
|
||||
@@ -153,6 +163,44 @@ ccs work-2 # Switch to second company account
|
||||
|
||||
---
|
||||
|
||||
## 📁 Shared Data Architecture
|
||||
|
||||
**v3.1 Shared Global Data**: Commands and skills are symlinked across all profiles via `~/.ccs/shared/`, eliminating duplication.
|
||||
|
||||
**Directory Structure**:
|
||||
```
|
||||
~/.ccs/
|
||||
├── shared/ # Shared across all profiles
|
||||
│ ├── commands/ # Custom slash commands
|
||||
│ └── skills/ # Claude Code skills
|
||||
├── instances/ # Profile-specific data
|
||||
│ ├── work/
|
||||
│ │ ├── commands@ → ~/.ccs/shared/commands/ # Symlink
|
||||
│ │ ├── skills@ → ~/.ccs/shared/skills/ # Symlink
|
||||
│ │ ├── settings.json # Profile-specific config
|
||||
│ │ └── sessions/ # Profile-specific sessions
|
||||
│ └── personal/
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- No duplication of commands/skills across profiles
|
||||
- Single source of truth for shared resources
|
||||
- Automatic migration from v3.0 (runs on first use)
|
||||
- Windows fallback: copies if symlinks unavailable (enable Developer Mode for true symlinks)
|
||||
|
||||
**What's Shared**:
|
||||
- `.claude/commands/` - Custom slash commands
|
||||
- `.claude/skills/` - Claude Code skills
|
||||
|
||||
**What's Profile-Specific**:
|
||||
- `settings.json` - API keys, credentials
|
||||
- `sessions/` - Conversation history
|
||||
- `todolists/` - Task tracking
|
||||
- `logs/` - Profile-specific logs
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
|
||||
**v3.0 Login-Per-Profile Model**: Each profile is an isolated Claude instance where users login directly. No credential copying or vault encryption.
|
||||
|
||||
+15
@@ -133,10 +133,20 @@ function handleHelpCommand() {
|
||||
// Configuration
|
||||
console.log(colored('Configuration:', 'cyan'));
|
||||
console.log(' Config File: ~/.ccs/config.json');
|
||||
console.log(' Profiles: ~/.ccs/profiles.json');
|
||||
console.log(' Instances: ~/.ccs/instances/');
|
||||
console.log(' Settings: ~/.ccs/*.settings.json');
|
||||
console.log(' Environment: CCS_CONFIG (override config path)');
|
||||
console.log('');
|
||||
|
||||
// Shared Data
|
||||
console.log(colored('Shared Data:', 'cyan'));
|
||||
console.log(' Commands: ~/.ccs/shared/commands/');
|
||||
console.log(' Skills: ~/.ccs/shared/skills/');
|
||||
console.log(' Agents: ~/.ccs/shared/agents/');
|
||||
console.log(' Note: Commands, skills, and agents are symlinked across all profiles');
|
||||
console.log('');
|
||||
|
||||
// Uninstall
|
||||
console.log(colored('Uninstall:', 'yellow'));
|
||||
console.log(' npm: npm uninstall -g @kaitranntt/ccs');
|
||||
@@ -252,6 +262,11 @@ async function main() {
|
||||
recovery.showRecoveryHints();
|
||||
}
|
||||
|
||||
// Run migration to shared structure (Phase 1: idempotent)
|
||||
const SharedManager = require('./shared-manager');
|
||||
const sharedManager = new SharedManager();
|
||||
sharedManager.migrateToSharedStructure();
|
||||
|
||||
// Detect profile
|
||||
const { profile, remainingArgs } = detectProfile(args);
|
||||
|
||||
|
||||
+9
-20
@@ -3,6 +3,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const SharedManager = require('./shared-manager');
|
||||
|
||||
/**
|
||||
* Instance Manager (Simplified)
|
||||
@@ -14,6 +15,7 @@ const os = require('os');
|
||||
class InstanceManager {
|
||||
constructor() {
|
||||
this.instancesDir = path.join(os.homedir(), '.ccs', 'instances');
|
||||
this.sharedManager = new SharedManager();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +58,7 @@ class InstanceManager {
|
||||
// Create base directory
|
||||
fs.mkdirSync(instancePath, { recursive: true, mode: 0o700 });
|
||||
|
||||
// Create Claude-expected subdirectories
|
||||
// Create Claude-expected subdirectories (profile-specific only)
|
||||
const subdirs = [
|
||||
'session-env',
|
||||
'todos',
|
||||
@@ -64,9 +66,7 @@ class InstanceManager {
|
||||
'file-history',
|
||||
'shell-snapshots',
|
||||
'debug',
|
||||
'.anthropic',
|
||||
'commands',
|
||||
'skills'
|
||||
'.anthropic'
|
||||
];
|
||||
|
||||
subdirs.forEach(dir => {
|
||||
@@ -76,7 +76,10 @@ class InstanceManager {
|
||||
}
|
||||
});
|
||||
|
||||
// Copy global configs if exist
|
||||
// Symlink shared directories (Phase 1: commands, skills)
|
||||
this.sharedManager.linkSharedDirectories(instancePath);
|
||||
|
||||
// Copy global configs if exist (settings.json only)
|
||||
this._copyGlobalConfigs(instancePath);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize instance for ${profileName}: ${error.message}`);
|
||||
@@ -158,26 +161,12 @@ class InstanceManager {
|
||||
_copyGlobalConfigs(instancePath) {
|
||||
const globalConfigDir = path.join(os.homedir(), '.claude');
|
||||
|
||||
// Copy settings.json if exists
|
||||
// Copy settings.json only (commands/skills are now symlinked to shared/)
|
||||
const globalSettings = path.join(globalConfigDir, 'settings.json');
|
||||
if (fs.existsSync(globalSettings)) {
|
||||
const instanceSettings = path.join(instancePath, 'settings.json');
|
||||
fs.copyFileSync(globalSettings, instanceSettings);
|
||||
}
|
||||
|
||||
// Copy commands directory if exists
|
||||
const globalCommands = path.join(globalConfigDir, 'commands');
|
||||
if (fs.existsSync(globalCommands)) {
|
||||
const instanceCommands = path.join(instancePath, 'commands');
|
||||
this._copyDirectory(globalCommands, instanceCommands);
|
||||
}
|
||||
|
||||
// Copy skills directory if exists
|
||||
const globalSkills = path.join(globalConfigDir, 'skills');
|
||||
if (fs.existsSync(globalSkills)) {
|
||||
const instanceSkills = path.join(instancePath, 'skills');
|
||||
this._copyDirectory(globalSkills, instanceSkills);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* SharedManager - Manages symlinked shared directories for CCS
|
||||
* Phase 1: Shared Global Data via Symlinks
|
||||
*
|
||||
* Purpose: Eliminates duplication of commands/skills across profile instances
|
||||
* by symlinking to a single ~/.ccs/shared/ directory.
|
||||
*/
|
||||
class SharedManager {
|
||||
constructor() {
|
||||
this.homeDir = os.homedir();
|
||||
this.sharedDir = path.join(this.homeDir, '.ccs', 'shared');
|
||||
this.instancesDir = path.join(this.homeDir, '.ccs', 'instances');
|
||||
this.sharedDirs = ['commands', 'skills', 'agents'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure shared directories exist
|
||||
*/
|
||||
ensureSharedDirectories() {
|
||||
// Create shared directory
|
||||
if (!fs.existsSync(this.sharedDir)) {
|
||||
fs.mkdirSync(this.sharedDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
// Create shared subdirectories
|
||||
for (const dir of this.sharedDirs) {
|
||||
const dirPath = path.join(this.sharedDir, dir);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link shared directories to instance
|
||||
* @param {string} instancePath - Path to instance directory
|
||||
*/
|
||||
linkSharedDirectories(instancePath) {
|
||||
this.ensureSharedDirectories();
|
||||
|
||||
for (const dir of this.sharedDirs) {
|
||||
const linkPath = path.join(instancePath, dir);
|
||||
const targetPath = path.join(this.sharedDir, dir);
|
||||
|
||||
// Remove existing directory/link
|
||||
if (fs.existsSync(linkPath)) {
|
||||
fs.rmSync(linkPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
try {
|
||||
fs.symlinkSync(targetPath, linkPath, 'dir');
|
||||
} catch (err) {
|
||||
// Windows fallback: copy directory if symlink fails
|
||||
if (process.platform === 'win32') {
|
||||
this._copyDirectory(targetPath, linkPath);
|
||||
console.log(`[!] Symlink failed for ${dir}, copied instead (enable Developer Mode)`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate existing instances to shared structure
|
||||
* Idempotent: Safe to run multiple times
|
||||
*/
|
||||
migrateToSharedStructure() {
|
||||
// Check if migration is needed (shared dirs exist but are empty)
|
||||
const needsMigration = !fs.existsSync(this.sharedDir) ||
|
||||
this.sharedDirs.every(dir => {
|
||||
const dirPath = path.join(this.sharedDir, dir);
|
||||
if (!fs.existsSync(dirPath)) return true;
|
||||
try {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
return files.length === 0; // Empty directory needs migration
|
||||
} catch (err) {
|
||||
return true; // If we can't read it, assume it needs migration
|
||||
}
|
||||
});
|
||||
|
||||
if (!needsMigration) {
|
||||
return; // Already migrated with content
|
||||
}
|
||||
|
||||
// Create shared directories
|
||||
this.ensureSharedDirectories();
|
||||
|
||||
// Copy from ~/.claude/ (actual Claude CLI directory)
|
||||
const claudeDir = path.join(this.homeDir, '.claude');
|
||||
|
||||
if (fs.existsSync(claudeDir)) {
|
||||
// Copy commands to shared (if exists)
|
||||
const commandsPath = path.join(claudeDir, 'commands');
|
||||
if (fs.existsSync(commandsPath)) {
|
||||
this._copyDirectory(commandsPath, path.join(this.sharedDir, 'commands'));
|
||||
}
|
||||
|
||||
// Copy skills to shared (if exists)
|
||||
const skillsPath = path.join(claudeDir, 'skills');
|
||||
if (fs.existsSync(skillsPath)) {
|
||||
this._copyDirectory(skillsPath, path.join(this.sharedDir, 'skills'));
|
||||
}
|
||||
|
||||
// Copy agents to shared (if exists)
|
||||
const agentsPath = path.join(claudeDir, 'agents');
|
||||
if (fs.existsSync(agentsPath)) {
|
||||
this._copyDirectory(agentsPath, path.join(this.sharedDir, 'agents'));
|
||||
}
|
||||
}
|
||||
|
||||
// Update all instances to use symlinks
|
||||
if (fs.existsSync(this.instancesDir)) {
|
||||
const instances = fs.readdirSync(this.instancesDir);
|
||||
|
||||
for (const instance of instances) {
|
||||
const instancePath = path.join(this.instancesDir, instance);
|
||||
if (fs.statSync(instancePath).isDirectory()) {
|
||||
this.linkSharedDirectories(instancePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[OK] Migrated to shared structure');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy directory recursively (fallback for Windows)
|
||||
* @param {string} src - Source directory
|
||||
* @param {string} dest - Destination directory
|
||||
* @private
|
||||
*/
|
||||
_copyDirectory(src, dest) {
|
||||
if (!fs.existsSync(src)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
this._copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SharedManager;
|
||||
@@ -0,0 +1,239 @@
|
||||
# Why Session Sharing Fails Across Profiles: Technical Analysis
|
||||
|
||||
## Overview
|
||||
|
||||
Claude CLI sessions cannot be directly shared across CCS profiles due to authentication architecture constraints. When a user starts a thread on a "work" profile and the account expires, resuming from a "personal" profile fails despite the local session file being present.
|
||||
|
||||
**Core Problem**: Thread IDs stored locally, but API-side validation requires matching OAuth credentials. Session "ownership" bound to account that created it, preventing cross-account continuation.
|
||||
|
||||
## Technical Architecture: How Claude CLI Sessions Work
|
||||
|
||||
### Session Storage Structure
|
||||
|
||||
```
|
||||
~/.claude/
|
||||
├── .credentials.json # OAuth tokens (access, refresh, expiry)
|
||||
├── projects/
|
||||
│ └── <project-hash>/
|
||||
│ └── <sessionId>.jsonl # Thread conversation history
|
||||
├── session-env/ # Ephemeral runtime state (empty dirs)
|
||||
└── todos/ # Task tracking
|
||||
```
|
||||
|
||||
### Session File Format (JSONL)
|
||||
|
||||
Each thread stored as newline-delimited JSON:
|
||||
|
||||
```jsonl
|
||||
{"type":"user","sessionId":"<uuid>","message":{"role":"user","content":"..."},"uuid":"<msg-id>","timestamp":"..."}
|
||||
{"type":"assistant","sessionId":"<uuid>","message":{"role":"assistant","content":[...]},"requestId":"req_...","uuid":"<msg-id>","timestamp":"..."}
|
||||
```
|
||||
|
||||
**Key Fields**:
|
||||
- `sessionId`: Thread UUID (e.g., `58921318-0aed-4238-b6cb-d0d6222fc095`)
|
||||
- `message`: User/assistant conversation turns
|
||||
- `requestId`: API request ID (server-generated, scoped to original auth)
|
||||
- **No embedded auth tokens**: Credentials stored separately in `.credentials.json`
|
||||
|
||||
### OAuth 2.0 Authentication Model
|
||||
|
||||
**Credential File** (`~/.claude/.credentials.json`):
|
||||
```json
|
||||
{
|
||||
"claudeAiOauth": {
|
||||
"accessToken": "sk-ant-oat01-...",
|
||||
"refreshToken": "sk-ant-ort01-...",
|
||||
"expiresAt": 1762851849221,
|
||||
"scopes": ["user:inference", "user:profile"],
|
||||
"subscriptionType": "max"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. User runs `claude --resume <sessionId>`
|
||||
2. CLI reads `~/.claude/projects/<project>/<sessionId>.jsonl`
|
||||
3. CLI reconstructs full conversation history
|
||||
4. CLI submits request to API with OAuth token from `.credentials.json`
|
||||
5. **API validates token + session ownership (inferred)**
|
||||
6. API responds (session context maintained client-side only)
|
||||
|
||||
### API-Side Validation
|
||||
|
||||
Claude API is stateless. "Sessions" are client-side constructs:
|
||||
- Thread ID alone does NOT authorize continuation
|
||||
- Each request requires valid OAuth token
|
||||
- Server likely validates session creator identity (undocumented but inferred from org ID headers)
|
||||
- No server-side session state tied to thread IDs
|
||||
|
||||
## Why Direct Sharing Fails: Core Technical Blockers
|
||||
|
||||
| Component | Transferable? | Blocker |
|
||||
|-----------|--------------|---------|
|
||||
| Session JSONL file | ✅ Yes | None (plain text) |
|
||||
| Thread ID (UUID) | ✅ Yes | None (identifier only) |
|
||||
| Conversation history | ✅ Yes | None (embedded in JSONL) |
|
||||
| OAuth token | ❌ No | Account-specific, expires |
|
||||
| API authorization | ❌ No | Server-side session ownership validation |
|
||||
|
||||
### OAuth Token Mismatch
|
||||
|
||||
- Work profile session created with `work_oauth_token`
|
||||
- Personal profile uses `personal_oauth_token`
|
||||
- API rejects requests where token doesn't match session creator (inferred)
|
||||
|
||||
### Organization ID Boundaries
|
||||
|
||||
Anthropic returns `anthropic-organization-id` header in responses. Sessions may be scoped to:
|
||||
- User account ID
|
||||
- Organization ID (for team accounts)
|
||||
- Subscription tier
|
||||
|
||||
Cross-account access would violate organizational isolation.
|
||||
|
||||
### Session Ownership Enforcement
|
||||
|
||||
No documented API for "transferring" session ownership. Thread ID is identifier, not authorization:
|
||||
- Like a file path: knowing the path doesn't grant read access
|
||||
- Requires matching credentials to prove ownership
|
||||
- No "share session" endpoint exists in Claude API
|
||||
|
||||
### Documentation Gaps
|
||||
|
||||
- No cross-account access docs in official Claude API documentation
|
||||
- No session sharing/transfer APIs in CLI or Agent SDK
|
||||
- Security model not explicitly documented (inferred from OAuth behavior)
|
||||
|
||||
## Security Implications: Why This Is By Design
|
||||
|
||||
### Risks if Session Sharing Were Possible
|
||||
|
||||
| Risk | Impact |
|
||||
|------|--------|
|
||||
| Context Leak | Conversation history exposed to unintended accounts |
|
||||
| Privilege Escalation | Lower-tier account accessing higher-tier sessions |
|
||||
| Organizational Data Breach | Work conversations leaked to personal accounts |
|
||||
| Audit Trail Corruption | Session actions attributed to wrong user |
|
||||
|
||||
### Current Security Posture
|
||||
|
||||
- **Credential Isolation**: OAuth tokens stored per-profile (CCS architecture)
|
||||
- **POSIX Permissions**: `~/.claude/` readable only by user (`700` permissions)
|
||||
- **No Session Encryption**: JSONL files plain text (relies on OS security)
|
||||
- **Profile Separation**: CCS v3.0 login-per-profile prevents credential mixing
|
||||
|
||||
### Design Intent
|
||||
|
||||
Session isolation prevents:
|
||||
1. **Cross-account context leakage**: Alice's work threads stay with work account
|
||||
2. **Subscription boundary violations**: Free account can't "resume" Pro sessions
|
||||
3. **Organizational data governance**: Company data stays within company accounts
|
||||
|
||||
## Alternative Approaches: What Works Instead
|
||||
|
||||
### Manual JSONL Copy (Workaround, Not Recommended)
|
||||
|
||||
**Process**:
|
||||
1. Copy session file: `~/.ccs/instances/work/.claude/projects/<project>/<sessionId>.jsonl`
|
||||
2. Generate new UUID: `new_id=$(uuidgen | tr '[:upper:]' '[:lower:]')`
|
||||
3. Replace `sessionId` in JSONL: `sed 's/<old_id>/<new_id>/g' file.jsonl > new.jsonl`
|
||||
4. Place in personal profile: `~/.ccs/instances/personal/.claude/projects/<project>/<new_id>.jsonl`
|
||||
5. Resume: `ccs personal` → `claude --resume <new_id>`
|
||||
|
||||
**Issues**:
|
||||
- Fragile (breaks if JSONL structure changes)
|
||||
- No UUID regeneration for `parentUuid`, message `uuid` fields
|
||||
- Risk of malformed JSON breaking CLI
|
||||
- Manual process prone to errors
|
||||
|
||||
### Context Export/Import (PARTIALLY FEASIBLE, Not Implemented)
|
||||
|
||||
**Concept**:
|
||||
```bash
|
||||
# Work profile (before expiration)
|
||||
ccs export-context --session <id> --output work-context.json
|
||||
|
||||
# Personal profile (after expiration)
|
||||
ccs import-context work-context.json --new-session
|
||||
```
|
||||
|
||||
**Process**:
|
||||
1. **Export**: Read JSONL → extract message history → strip metadata → save portable JSON
|
||||
2. **Import**: Parse JSON → generate new session UUID → reconstruct JSONL with personal auth context
|
||||
3. **Resume**: Use new session ID with personal OAuth token
|
||||
|
||||
**Advantages**:
|
||||
- ✅ Preserves conversation history
|
||||
- ✅ New session = clear ownership (personal account)
|
||||
- ✅ No cross-account authorization issues
|
||||
- ✅ Explicit user action (no silent credential sharing)
|
||||
- ✅ Audit-friendly (new session ID)
|
||||
|
||||
**Status**: DEFERRED (not implemented in CCS v3.0)
|
||||
|
||||
### User Experience Flow (Proposed)
|
||||
|
||||
```
|
||||
$ ccs export-session 58921318-0aed-4238-b6cb-d0d6222fc095
|
||||
✓ Exported 47 messages to ~/ccs-export-58921318.json
|
||||
|
||||
[Account expires]
|
||||
|
||||
$ ccs personal
|
||||
$ ccs import-session ~/ccs-export-58921318.json
|
||||
✓ Imported as new thread: a1b2c3d4-5678-9012-3456-789012345678
|
||||
✓ Resume with: claude --resume a1b2c3d4-5678-9012-3456-789012345678
|
||||
|
||||
$ claude --resume a1b2c3d4-5678-9012-3456-789012345678 "Continue from earlier"
|
||||
[Claude continues with full context under personal auth]
|
||||
```
|
||||
|
||||
## Recommendations: Clear Guidance
|
||||
|
||||
### REJECT: Direct Session Sharing
|
||||
|
||||
**Do NOT implement**:
|
||||
- Copying session files between profiles without regeneration
|
||||
- Reusing thread IDs across accounts
|
||||
- "Resuming" sessions created by different OAuth tokens
|
||||
|
||||
**Reason**: Violates inferred security model; likely fails API-side validation.
|
||||
|
||||
### DEFER: Export/Import Feature
|
||||
|
||||
**Status**: Feasible but not critical for CCS v3.0.
|
||||
|
||||
**Priority**: Phase 3 (post-launch).
|
||||
|
||||
**Effort**: Medium (~3-5 days implementation + testing).
|
||||
|
||||
**Dependencies**:
|
||||
- JSONL parsing/reconstruction logic
|
||||
- UUID generation for new sessions
|
||||
- Profile-aware file I/O
|
||||
|
||||
### Document: Manual Workaround
|
||||
|
||||
**For Advanced Users** (FAQ section):
|
||||
|
||||
1. Manually copy JSONL file
|
||||
2. Edit `sessionId` fields (use `jq` or `sed`)
|
||||
3. Regenerate UUIDs for messages (optional but safer)
|
||||
4. Place in target profile's `.claude/projects/` directory
|
||||
5. Resume with `claude --resume <new_id>`
|
||||
|
||||
**Warning**: Unsupported; may break with Claude CLI updates.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
1. **Does Claude API enforce server-side session ownership?**
|
||||
Inferred from OAuth architecture; no official docs confirm. Would require API testing.
|
||||
|
||||
2. **What is `~/.claude/session-env/` used for?**
|
||||
Contains empty UUID directories; possibly ephemeral runtime state. Doesn't affect persistence.
|
||||
|
||||
3. **Can malformed JSONL crash Claude CLI?**
|
||||
Likely yes (JSON parsing errors). Manual editing risky without validation.
|
||||
|
||||
4. **What happens if imported session references inaccessible resources?**
|
||||
E.g., work git repo personal account can't access. Claude will continue but may request clarification.
|
||||
@@ -1,418 +0,0 @@
|
||||
# CCS Testing Requirements & Procedures
|
||||
|
||||
**Version:** 2.4.4
|
||||
**Last Updated:** 2025-11-05
|
||||
**Status:** Active
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the comprehensive testing requirements and procedures for the CCS (Claude Code Switch) project. Following these guidelines ensures consistent, reliable, and cross-platform compatible releases across both npm package and shell installer methods.
|
||||
|
||||
## Test Suite Structure
|
||||
|
||||
### Core Test Files
|
||||
|
||||
| Test Type | Files | Purpose | Coverage | Platforms |
|
||||
|-----------|-------|---------|----------|-----------|
|
||||
| **npm Package Tests** | `tests/npm/*.test.js` | npm package functionality | 4 files | Cross-platform |
|
||||
| **Unit Tests** | `tests/shared/unit/*.test.js` | Core utilities testing | 1 file | Cross-platform |
|
||||
| **Native Shell Tests** | `tests/native/unix/*.sh` | Shell installer testing | 3 files | Unix/Linux/macOS |
|
||||
| **Edge Case Tests** | `tests/edge-cases.sh` | Comprehensive edge cases | 1 file | Unix/Linux/macOS |
|
||||
| **Comprehensive Testing** | `plans/251105-*/` | 5-phase testing framework | 5 phases | Cross-platform |
|
||||
|
||||
### Test Categories
|
||||
|
||||
#### 1. npm Package Tests (39 tests)
|
||||
**Files:** `tests/npm/*.test.js`, `tests/shared/unit/*.test.js`
|
||||
|
||||
**Coverage Areas:**
|
||||
- **Installation Testing** (npm global install, postinstall script)
|
||||
- **Configuration Management** (auto-creation, JSON validation)
|
||||
- **CLI Functionality** (version, help, profile switching)
|
||||
- **Error Handling** (invalid profiles, missing configs)
|
||||
- **Performance Testing** (startup times, memory usage)
|
||||
- **Cross-Platform Testing** (bash, zsh, different PATH configurations)
|
||||
|
||||
#### 2. Shell Installer Tests (57 tests)
|
||||
**Files:** `tests/native/unix/*.sh`, `tests/edge-cases.sh`
|
||||
|
||||
**Coverage Areas:**
|
||||
- **Installation Process** (curl installer, directory creation)
|
||||
- **Uninstall Functionality** (complete cleanup, PATH restoration)
|
||||
- **Shell Script Logic** (jq integration, platform detection)
|
||||
- **Edge Cases** (partial installs, error scenarios)
|
||||
- **Configuration Management** (profile handling, file validation)
|
||||
- **Integration Testing** (Claude CLI connectivity)
|
||||
|
||||
#### 3. Comprehensive Cross-Compatibility Testing
|
||||
**Framework:** 5-Phase Testing (`plans/251105-comprehensive-ccs-testing/`)
|
||||
|
||||
**Phases:**
|
||||
1. **Environment Assessment & Cleanup** (System state validation)
|
||||
2. **npm Package Testing** (Full npm package validation)
|
||||
3. **Shell Installer Testing** (Shell method validation)
|
||||
4. **Cross-Compatibility Testing** (Migration between methods)
|
||||
5. **Final Cleanup & Validation** (System restoration)
|
||||
|
||||
**Key Findings from Latest Testing:**
|
||||
- npm version: 21ms startup, Node.js based, cross-platform
|
||||
- Shell version: 5ms startup (4x faster), bash based, Unix-like only
|
||||
- Configuration: Fully compatible between methods
|
||||
- Migration: Seamless switching between installation methods
|
||||
|
||||
## Testing Environment Requirements
|
||||
|
||||
### Environment Variable Isolation
|
||||
|
||||
**Critical Requirement:** All tests must use isolated HOME directories to prevent impact on user data.
|
||||
|
||||
**Implementation Pattern:**
|
||||
```bash
|
||||
# Unix/Linux/macOS
|
||||
HOME=/tmp/test-ccs-home ./ccs --install
|
||||
HOME=/tmp/test-ccs-home ./ccs --uninstall
|
||||
|
||||
# Windows PowerShell
|
||||
$env:HOME = "C:\temp\test-ccs-home"
|
||||
.\ccs.ps1 --install
|
||||
.\ccs.ps1 --uninstall
|
||||
```
|
||||
|
||||
**Validation Requirements:**
|
||||
- ✅ Install uses test directory (not `~/.claude`)
|
||||
- ✅ Uninstall removes files from test directory only
|
||||
- ✅ Real user directories completely unaffected
|
||||
- ✅ Perfect test isolation achieved
|
||||
|
||||
### Cross-Platform Compatibility
|
||||
|
||||
**Platform-Specific Patterns:**
|
||||
- **Bash Version:** Uses `$HOME/.claude` directly
|
||||
- **PowerShell Version:** Uses HOME-first pattern with USERPROFILE fallback
|
||||
|
||||
**PowerShell Pattern:**
|
||||
```powershell
|
||||
$HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
|
||||
```
|
||||
|
||||
## Test Execution Procedures
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Clean Test Environment**
|
||||
```bash
|
||||
# Remove any existing CCS installation
|
||||
rm -rf ~/.ccs ~/.local/bin/ccs
|
||||
```
|
||||
|
||||
2. **Required Tools**
|
||||
- bash 3.2+ (Unix/Linux/macOS)
|
||||
- PowerShell 5.1+ (Windows)
|
||||
- Claude CLI 2.0.31+
|
||||
- jq 1.6+ (optional, for JSON validation)
|
||||
|
||||
### Running Tests
|
||||
|
||||
#### Unix/Linux/macOS
|
||||
```bash
|
||||
# Navigate to CCS directory
|
||||
cd /path/to/ccs
|
||||
|
||||
# Run uninstall tests
|
||||
./tests/uninstall-test.sh
|
||||
|
||||
# Run edge case tests
|
||||
./tests/edge-cases.sh
|
||||
|
||||
# Run all tests (full suite)
|
||||
./tests/uninstall-test.sh && ./tests/edge-cases.sh
|
||||
```
|
||||
|
||||
#### Windows
|
||||
```powershell
|
||||
# Navigate to CCS directory
|
||||
cd C:\path\to\ccs
|
||||
|
||||
# Run uninstall tests
|
||||
.\tests\uninstall-test.ps1
|
||||
|
||||
# Run edge case tests
|
||||
.\tests\edge-cases.ps1
|
||||
|
||||
# Run all tests (full suite)
|
||||
.\tests\uninstall-test.ps1; .\tests\edge-cases.ps1
|
||||
```
|
||||
|
||||
### Expected Results
|
||||
|
||||
**Success Criteria:**
|
||||
- **Total Test Pass Rate:** 100%
|
||||
- **Individual Test Suites:** Each must pass 100%
|
||||
- **Environment Isolation:** No impact on user data
|
||||
- **Cross-Platform Consistency:** Identical behavior across platforms
|
||||
|
||||
**Sample Output:**
|
||||
```
|
||||
=== CCS Uninstall Test Results ===
|
||||
Total Tests: 20
|
||||
Passed: 20 (100%)
|
||||
Failed: 0 (0%)
|
||||
Status: ✅ ALL TESTS PASSED
|
||||
```
|
||||
|
||||
## Quality Assurance Standards
|
||||
|
||||
### Code Quality Requirements
|
||||
|
||||
1. **Syntax Validation**
|
||||
```bash
|
||||
# Bash syntax check
|
||||
bash -n ccs
|
||||
bash -n install.sh
|
||||
bash -n uninstall.sh
|
||||
|
||||
# PowerShell syntax check
|
||||
Get-Command Test-Path -Syntax ccs.ps1
|
||||
```
|
||||
|
||||
2. **Pattern Consistency**
|
||||
- HOME-first environment variable pattern
|
||||
- Consistent error handling
|
||||
- Uniform output formatting
|
||||
|
||||
3. **Security Validation**
|
||||
- No impact on user data
|
||||
- Proper file permissions
|
||||
- No unauthorized directory access
|
||||
|
||||
### Functional Testing Requirements
|
||||
|
||||
1. **Happy Path Testing**
|
||||
- Standard operations work perfectly
|
||||
- Default behaviors function as expected
|
||||
- User workflows complete successfully
|
||||
|
||||
2. **Edge Case Handling**
|
||||
- Robust handling of unexpected inputs
|
||||
- Graceful failure modes
|
||||
- Clear error messages with actionable solutions
|
||||
|
||||
3. **Integration Testing**
|
||||
- CLI integration with all commands
|
||||
- Path operations work correctly
|
||||
- Environment variable handling is reliable
|
||||
|
||||
## Test Coverage Metrics
|
||||
|
||||
### Current Coverage (v2.4.5)
|
||||
|
||||
| Test Category | Tests | Pass Rate | Status |
|
||||
|---------------|-------|-----------|--------|
|
||||
| npm Package Tests | 39 | 100% | ✅ Complete |
|
||||
| Unit Tests | 3 | 100% | ✅ Complete |
|
||||
| Shell Installer Tests | 57 | 100% | ✅ Complete |
|
||||
| Cross-Compatibility Tests | 15 | 100% | ✅ Complete |
|
||||
| Environment Isolation | 114 | 100% | ✅ Complete |
|
||||
| **Total Coverage** | **114** | **100%** | **✅ Complete** |
|
||||
|
||||
### Coverage Goals for Future Releases
|
||||
|
||||
| Metric | Target | Current Status |
|
||||
|--------|--------|----------------|
|
||||
| Test Pass Rate | >95% | 100% ✅ |
|
||||
| Cross-Platform Coverage | 100% | 100% ✅ |
|
||||
| Edge Case Coverage | >90% | 100% ✅ |
|
||||
| Environment Isolation | 100% | 100% ✅ |
|
||||
| Security Validation | 100% | 100% ✅ |
|
||||
| npm Package Testing | 100% | 100% ✅ |
|
||||
| Installation Method Compatibility | 100% | 100% ✅ |
|
||||
|
||||
## Automated Testing Integration
|
||||
|
||||
### CI/CD Pipeline Requirements
|
||||
|
||||
**Recommended Integration:**
|
||||
```yaml
|
||||
# GitHub Actions example
|
||||
- name: Run CCS Tests
|
||||
run: |
|
||||
npm test
|
||||
# This runs: npm run test:unit && npm run test:npm
|
||||
```
|
||||
|
||||
**Enhanced Pipeline:**
|
||||
```yaml
|
||||
# Comprehensive testing
|
||||
- name: Environment Assessment
|
||||
run: ./scripts/clean-test-environment.sh
|
||||
|
||||
- name: npm Package Testing
|
||||
run: npm test
|
||||
|
||||
- name: Shell Installer Testing
|
||||
run: ./tests/native/unix/installer-tests.sh
|
||||
|
||||
- name: Cross-Compatibility Testing
|
||||
run: ./scripts/test-installation-methods.sh
|
||||
```
|
||||
|
||||
**Test Automation Benefits:**
|
||||
- Consistent test execution across all installation methods
|
||||
- Early detection of regressions
|
||||
- Cross-platform validation
|
||||
- Automated quality gates
|
||||
- Installation method compatibility verification
|
||||
|
||||
### Performance Testing
|
||||
|
||||
**Test Execution Benchmarks (Latest Results):**
|
||||
- **npm Package Tests:** ~45 seconds
|
||||
- **Shell Installer Tests:** ~60 seconds
|
||||
- **Cross-Compatibility Tests:** ~90 seconds
|
||||
- **Total Suite:** ~3-4 minutes (comprehensive 5-phase testing)
|
||||
- **Memory Usage:** Minimal
|
||||
- **Disk I/O:** Controlled and temporary
|
||||
|
||||
**Performance Comparison:**
|
||||
- **npm version startup:** 21ms
|
||||
- **Shell version startup:** 5ms (4x faster)
|
||||
- **Installation time:** npm (1.5s) vs Shell (3s)
|
||||
|
||||
## Test Maintenance Procedures
|
||||
|
||||
### When to Update Tests
|
||||
|
||||
1. **New Features Added**
|
||||
- Add corresponding test cases
|
||||
- Update test documentation
|
||||
- Validate cross-platform compatibility
|
||||
|
||||
2. **Bug Fixes Implemented**
|
||||
- Add regression tests for fixed bugs
|
||||
- Verify fix doesn't break existing functionality
|
||||
- Update test coverage metrics
|
||||
|
||||
3. **Platform Changes**
|
||||
- Test on new platform versions
|
||||
- Update platform-specific test cases
|
||||
- Validate compatibility
|
||||
|
||||
### Test Review Process
|
||||
|
||||
1. **Code Review Integration**
|
||||
- Tests reviewed alongside code changes
|
||||
- Ensure test coverage for new functionality
|
||||
- Validate test quality and effectiveness
|
||||
|
||||
2. **Release Validation**
|
||||
- Full test suite execution before releases
|
||||
- Cross-platform validation
|
||||
- Performance benchmarking
|
||||
|
||||
## Troubleshooting Test Failures
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Environment Variable Conflicts**
|
||||
- **Symptom:** Tests affecting user directories
|
||||
- **Solution:** Verify HOME isolation pattern
|
||||
- **Prevention:** Always use isolated test environments
|
||||
|
||||
2. **Permission Issues**
|
||||
- **Symptom:** File operation failures
|
||||
- **Solution:** Check file permissions and paths
|
||||
- **Prevention:** Validate prerequisites before testing
|
||||
|
||||
3. **Platform-Specific Failures**
|
||||
- **Symptom:** Tests pass on one platform, fail on another
|
||||
- **Solution:** Review platform-specific code paths
|
||||
- **Prevention:** Test on all supported platforms
|
||||
|
||||
### Debug Procedures
|
||||
|
||||
1. **Enable Verbose Output**
|
||||
```bash
|
||||
# Add debug flags to test scripts
|
||||
./tests/uninstall-test.sh --verbose
|
||||
```
|
||||
|
||||
2. **Isolate Failing Tests**
|
||||
```bash
|
||||
# Run individual test sections
|
||||
./tests/uninstall-test.sh --section=empty-uninstall
|
||||
```
|
||||
|
||||
3. **Validate Environment**
|
||||
```bash
|
||||
# Check environment variables
|
||||
echo "HOME: $HOME"
|
||||
echo "USERPROFILE: $USERPROFILE"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Test Development Guidelines
|
||||
|
||||
1. **Test Isolation**
|
||||
- Never modify user data during tests
|
||||
- Use temporary directories for all file operations
|
||||
- Clean up all test artifacts
|
||||
|
||||
2. **Cross-Platform Considerations**
|
||||
- Test on all supported platforms
|
||||
- Use platform-agnostic patterns where possible
|
||||
- Handle platform-specific differences explicitly
|
||||
|
||||
3. **Maintainability**
|
||||
- Clear test documentation
|
||||
- Consistent test structure
|
||||
- Reusable test utilities
|
||||
|
||||
### Continuous Improvement
|
||||
|
||||
1. **Test Coverage Analysis**
|
||||
- Regular coverage assessment
|
||||
- Identify untested code paths
|
||||
- Prioritize high-risk areas
|
||||
|
||||
2. **Performance Monitoring**
|
||||
- Track test execution times
|
||||
- Identify performance regressions
|
||||
- Optimize slow test cases
|
||||
|
||||
3. **Quality Metrics**
|
||||
- Monitor test pass rates
|
||||
- Track bug detection rates
|
||||
- Measure test effectiveness
|
||||
|
||||
## References
|
||||
|
||||
### Related Documentation
|
||||
- **Project Roadmap:** `/docs/project-roadmap.md`
|
||||
- **Implementation Plans:** `/plans/`
|
||||
- **Code Standards:** `/docs/code-standards.md`
|
||||
- **System Architecture:** `/docs/system-architecture.md`
|
||||
|
||||
### Test Reports
|
||||
- **Uninstall Test Report:** `/plans/reports/241103-from-qa-engineer-to-development-team-ccs-uninstall-testing-report.md`
|
||||
- **Code Review Reports:** `/plans/reports/251103-code-review-phase1-phase2.md`
|
||||
|
||||
### External Resources
|
||||
- **Claude CLI Documentation:** https://docs.anthropic.com/claude/reference/claude-cli
|
||||
- **PowerShell Best Practices:** https://docs.microsoft.com/en-us/powershell/scripting/dev-cross-plat/writing-portable-cmdlets
|
||||
|
||||
---
|
||||
|
||||
**Maintained By:** Development Team
|
||||
**Review Frequency:** After major releases or significant code changes
|
||||
**Last Updated:** 2025-11-05
|
||||
|
||||
**Recent Changes:**
|
||||
- Added npm package testing framework (39 tests)
|
||||
- Comprehensive cross-compatibility testing (5-phase framework)
|
||||
- Updated performance benchmarks and comparisons
|
||||
- Enhanced CI/CD pipeline recommendations
|
||||
|
||||
**Unresolved Questions:** None
|
||||
**Blockers:** None
|
||||
**Next Review:** Post v2.4.5 release or after next major code simplification
|
||||
@@ -31,7 +31,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or (
|
||||
# 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 = "3.0.2"
|
||||
$CcsVersion = "3.1.0"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if ($ScriptDir) {
|
||||
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
# 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="3.0.2"
|
||||
CCS_VERSION="3.1.0"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
# Version (updated by scripts/bump-version.sh)
|
||||
CCS_VERSION="3.0.2"
|
||||
CCS_VERSION="3.1.0"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
||||
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
|
||||
@@ -67,9 +67,15 @@ show_help() {
|
||||
echo -e " ${YELLOW}-v, --version${RESET} Show version and installation info"
|
||||
echo ""
|
||||
echo -e "${CYAN}Configuration:${RESET}"
|
||||
echo -e " Config: ~/.ccs/config.json"
|
||||
echo -e " Profiles: ~/.ccs/profiles.json"
|
||||
echo -e " Settings: ~/.ccs/*.settings.json"
|
||||
echo -e " Config: ~/.ccs/config.json"
|
||||
echo -e " Profiles: ~/.ccs/profiles.json"
|
||||
echo -e " Instances: ~/.ccs/instances/"
|
||||
echo -e " Settings: ~/.ccs/*.settings.json"
|
||||
echo ""
|
||||
echo -e "${CYAN}Shared Data:${RESET}"
|
||||
echo -e " Commands: ~/.ccs/shared/commands/"
|
||||
echo -e " Skills: ~/.ccs/shared/skills/"
|
||||
echo -e " Note: Commands, skills, and agents are symlinked across all profiles"
|
||||
echo ""
|
||||
echo -e "${CYAN}Documentation:${RESET}"
|
||||
echo -e " GitHub: ${CYAN}https://github.com/kaitranntt/ccs${RESET}"
|
||||
@@ -493,6 +499,73 @@ sanitize_profile_name() {
|
||||
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g'
|
||||
}
|
||||
|
||||
# Link shared directories (Phase 1: Shared Global Data)
|
||||
link_shared_directories() {
|
||||
local instance_path="$1"
|
||||
local shared_dir="$HOME/.ccs/shared"
|
||||
|
||||
# Ensure shared directories exist
|
||||
mkdir -p "$shared_dir"/{commands,skills,agents}
|
||||
|
||||
# Create symlinks (remove existing first if present)
|
||||
for dir in commands skills agents; do
|
||||
local link_path="$instance_path/$dir"
|
||||
local target_path="$shared_dir/$dir"
|
||||
|
||||
# Remove existing directory/link
|
||||
[[ -e "$link_path" ]] && rm -rf "$link_path"
|
||||
|
||||
# Create symlink
|
||||
ln -sf "$target_path" "$link_path"
|
||||
done
|
||||
}
|
||||
|
||||
# Migrate to shared structure (Phase 1: Auto-migration)
|
||||
migrate_to_shared_structure() {
|
||||
local shared_dir="$HOME/.ccs/shared"
|
||||
|
||||
# Check if migration is needed (shared dirs exist but are empty)
|
||||
if [[ -d "$shared_dir" ]]; then
|
||||
local needs_migration=false
|
||||
for dir in commands skills agents; do
|
||||
if [[ ! -d "$shared_dir/$dir" ]] || [[ -z "$(ls -A "$shared_dir/$dir" 2>/dev/null)" ]]; then
|
||||
needs_migration=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
[[ "$needs_migration" == "false" ]] && return 0
|
||||
fi
|
||||
|
||||
# Create shared directory
|
||||
mkdir -p "$shared_dir"/{commands,skills,agents}
|
||||
|
||||
# Copy from ~/.claude/ (actual Claude CLI directory)
|
||||
local claude_dir="$HOME/.claude"
|
||||
|
||||
if [[ -d "$claude_dir" ]]; then
|
||||
# Copy commands to shared (if exists)
|
||||
[[ -d "$claude_dir/commands" ]] && \
|
||||
cp -r "$claude_dir/commands"/* "$shared_dir/commands/" 2>/dev/null || true
|
||||
|
||||
# Copy skills to shared (if exists)
|
||||
[[ -d "$claude_dir/skills" ]] && \
|
||||
cp -r "$claude_dir/skills"/* "$shared_dir/skills/" 2>/dev/null || true
|
||||
|
||||
# Copy agents to shared (if exists)
|
||||
[[ -d "$claude_dir/agents" ]] && \
|
||||
cp -r "$claude_dir/agents"/* "$shared_dir/agents/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Update all instances to use symlinks
|
||||
for instance_path in "$INSTANCES_DIR"/*; do
|
||||
[[ -d "$instance_path" ]] || continue
|
||||
link_shared_directories "$instance_path"
|
||||
done
|
||||
|
||||
echo "[OK] Migrated to shared structure"
|
||||
}
|
||||
|
||||
# Initialize new instance directory
|
||||
initialize_instance() {
|
||||
local instance_path="$1"
|
||||
@@ -500,12 +573,15 @@ initialize_instance() {
|
||||
# Create base directory
|
||||
mkdir -m 0700 -p "$instance_path"
|
||||
|
||||
# Create subdirectories
|
||||
local subdirs=(session-env todos logs file-history shell-snapshots debug .anthropic commands skills)
|
||||
# Create subdirectories (profile-specific only)
|
||||
local subdirs=(session-env todos logs file-history shell-snapshots debug .anthropic)
|
||||
for dir in "${subdirs[@]}"; do
|
||||
mkdir -m 0700 -p "$instance_path/$dir"
|
||||
done
|
||||
|
||||
# Symlink shared directories
|
||||
link_shared_directories "$instance_path"
|
||||
|
||||
# Copy global configs (optional)
|
||||
copy_global_configs "$instance_path"
|
||||
}
|
||||
@@ -527,17 +603,9 @@ copy_global_configs() {
|
||||
local instance_path="$1"
|
||||
local global_claude="$HOME/.claude"
|
||||
|
||||
# Copy settings.json
|
||||
# Copy settings.json only (commands/skills are now symlinked to shared/)
|
||||
[[ -f "$global_claude/settings.json" ]] && \
|
||||
cp "$global_claude/settings.json" "$instance_path/settings.json" 2>/dev/null || true
|
||||
|
||||
# Copy commands/
|
||||
[[ -d "$global_claude/commands" ]] && \
|
||||
cp -r "$global_claude/commands" "$instance_path/" 2>/dev/null || true
|
||||
|
||||
# Copy skills/
|
||||
[[ -d "$global_claude/skills" ]] && \
|
||||
cp -r "$global_claude/skills" "$instance_path/" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Ensure instance exists (lazy initialization)
|
||||
@@ -942,6 +1010,9 @@ auto_recover || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Run migration to shared structure (Phase 1: idempotent)
|
||||
migrate_to_shared_structure
|
||||
|
||||
# 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
|
||||
|
||||
+109
-19
@@ -12,7 +12,7 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Version (updated by scripts/bump-version.sh)
|
||||
$CcsVersion = "3.0.2"
|
||||
$CcsVersion = "3.1.0"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
|
||||
$ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
|
||||
@@ -123,9 +123,15 @@ function Show-Help {
|
||||
Write-ColorLine " -v, --version Show version and installation info" "Yellow"
|
||||
Write-Host ""
|
||||
Write-ColorLine "Configuration:" "Cyan"
|
||||
Write-Host " Config: ~/.ccs/config.json"
|
||||
Write-Host " Profiles: ~/.ccs/profiles.json"
|
||||
Write-Host " Settings: ~/.ccs/*.settings.json"
|
||||
Write-Host " Config: ~/.ccs/config.json"
|
||||
Write-Host " Profiles: ~/.ccs/profiles.json"
|
||||
Write-Host " Instances: ~/.ccs/instances/"
|
||||
Write-Host " Settings: ~/.ccs/*.settings.json"
|
||||
Write-Host ""
|
||||
Write-ColorLine "Shared Data:" "Cyan"
|
||||
Write-Host " Commands: ~/.ccs/shared/commands/"
|
||||
Write-Host " Skills: ~/.ccs/shared/skills/"
|
||||
Write-Host " Note: Commands, skills, and agents are symlinked across all profiles"
|
||||
Write-Host ""
|
||||
Write-ColorLine "Documentation:" "Cyan"
|
||||
Write-Host " GitHub: https://github.com/kaitranntt/ccs"
|
||||
@@ -422,28 +428,106 @@ function Set-InstancePermissions {
|
||||
Set-Acl -Path $Path -AclObject $Acl
|
||||
}
|
||||
|
||||
function Link-SharedDirectories {
|
||||
param([string]$InstancePath)
|
||||
|
||||
$SharedDir = "$env:USERPROFILE\.ccs\shared"
|
||||
|
||||
# Ensure shared directories exist
|
||||
@('commands', 'skills', 'agents') | ForEach-Object {
|
||||
$Dir = Join-Path $SharedDir $_
|
||||
if (-not (Test-Path $Dir)) {
|
||||
New-Item -ItemType Directory -Path $Dir -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Create symlinks (requires Windows Developer Mode or admin)
|
||||
@('commands', 'skills', 'agents') | ForEach-Object {
|
||||
$LinkPath = Join-Path $InstancePath $_
|
||||
$TargetPath = Join-Path $SharedDir $_
|
||||
|
||||
# Remove existing directory/link
|
||||
if (Test-Path $LinkPath) {
|
||||
Remove-Item $LinkPath -Recurse -Force
|
||||
}
|
||||
|
||||
# Try creating symlink (requires privileges)
|
||||
try {
|
||||
New-Item -ItemType SymbolicLink -Path $LinkPath -Target $TargetPath -Force | Out-Null
|
||||
} catch {
|
||||
# Fallback: Copy directory instead (suboptimal but functional)
|
||||
Copy-Item $TargetPath -Destination $LinkPath -Recurse -Force
|
||||
Write-Host "[!] Symlink failed for $_, copied instead (enable Developer Mode)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Migrate-SharedStructure {
|
||||
$SharedDir = "$env:USERPROFILE\.ccs\shared"
|
||||
|
||||
# Check if migration is needed (shared dirs exist but are empty)
|
||||
if (Test-Path $SharedDir) {
|
||||
$NeedsMigration = $false
|
||||
foreach ($Dir in @('commands', 'skills', 'agents')) {
|
||||
$DirPath = Join-Path $SharedDir $Dir
|
||||
if (-not (Test-Path $DirPath) -or (Get-ChildItem $DirPath -ErrorAction SilentlyContinue).Count -eq 0) {
|
||||
$NeedsMigration = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $NeedsMigration) { return }
|
||||
}
|
||||
|
||||
# Create shared directory
|
||||
@('commands', 'skills', 'agents') | ForEach-Object {
|
||||
$Dir = Join-Path $SharedDir $_
|
||||
New-Item -ItemType Directory -Path $Dir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Copy from ~/.claude/ (actual Claude CLI directory)
|
||||
$ClaudeDir = "$env:USERPROFILE\.claude"
|
||||
|
||||
if (Test-Path $ClaudeDir) {
|
||||
# Copy commands to shared (if exists)
|
||||
$CommandsPath = Join-Path $ClaudeDir "commands"
|
||||
if (Test-Path $CommandsPath) {
|
||||
Copy-Item "$CommandsPath\*" -Destination "$SharedDir\commands\" -Recurse -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Copy skills to shared (if exists)
|
||||
$SkillsPath = Join-Path $ClaudeDir "skills"
|
||||
if (Test-Path $SkillsPath) {
|
||||
Copy-Item "$SkillsPath\*" -Destination "$SharedDir\skills\" -Recurse -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Copy agents to shared (if exists)
|
||||
$AgentsPath = Join-Path $ClaudeDir "agents"
|
||||
if (Test-Path $AgentsPath) {
|
||||
Copy-Item "$AgentsPath\*" -Destination "$SharedDir\agents\" -Recurse -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Update all instances to use symlinks
|
||||
if (Test-Path $InstancesDir) {
|
||||
Get-ChildItem $InstancesDir -Directory | ForEach-Object {
|
||||
Link-SharedDirectories $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[OK] Migrated to shared structure"
|
||||
}
|
||||
|
||||
function Copy-GlobalConfigs {
|
||||
param([string]$InstancePath)
|
||||
|
||||
$GlobalClaude = "$env:USERPROFILE\.claude"
|
||||
|
||||
# Copy settings.json
|
||||
# Copy settings.json only (commands/skills are now symlinked to shared/)
|
||||
$GlobalSettings = Join-Path $GlobalClaude "settings.json"
|
||||
if (Test-Path $GlobalSettings) {
|
||||
Copy-Item $GlobalSettings -Destination (Join-Path $InstancePath "settings.json") -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Copy commands/
|
||||
$GlobalCommands = Join-Path $GlobalClaude "commands"
|
||||
if (Test-Path $GlobalCommands) {
|
||||
Copy-Item $GlobalCommands -Destination $InstancePath -Recurse -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Copy skills/
|
||||
$GlobalSkills = Join-Path $GlobalClaude "skills"
|
||||
if (Test-Path $GlobalSkills) {
|
||||
Copy-Item $GlobalSkills -Destination $InstancePath -Recurse -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-Instance {
|
||||
@@ -453,15 +537,18 @@ function Initialize-Instance {
|
||||
New-Item -ItemType Directory -Path $InstancePath -Force | Out-Null
|
||||
Set-InstancePermissions $InstancePath
|
||||
|
||||
# Create subdirectories
|
||||
# Create subdirectories (profile-specific only)
|
||||
$Subdirs = @('session-env', 'todos', 'logs', 'file-history',
|
||||
'shell-snapshots', 'debug', '.anthropic', 'commands', 'skills')
|
||||
'shell-snapshots', 'debug', '.anthropic')
|
||||
|
||||
foreach ($Dir in $Subdirs) {
|
||||
$DirPath = Join-Path $InstancePath $Dir
|
||||
New-Item -ItemType Directory -Path $DirPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# Symlink shared directories
|
||||
Link-SharedDirectories $InstancePath
|
||||
|
||||
# Copy global configs
|
||||
Copy-GlobalConfigs $InstancePath
|
||||
}
|
||||
@@ -913,6 +1000,9 @@ if (-not (Invoke-AutoRecovery)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Run migration to shared structure (Phase 1: idempotent)
|
||||
Migrate-SharedStructure
|
||||
|
||||
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
|
||||
if ($RemainingArgs.Count -eq 0 -or $RemainingArgs[0] -match '^-') {
|
||||
# No args or first arg is a flag → use default profile
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kaitranntt/ccs",
|
||||
"version": "3.0.2",
|
||||
"version": "3.1.0",
|
||||
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -72,6 +72,23 @@ function createConfigFiles() {
|
||||
console.log('[OK] Created directory: ~/.ccs/');
|
||||
}
|
||||
|
||||
// Create ~/.ccs/shared/ directory structure (Phase 1)
|
||||
const sharedDir = path.join(ccsDir, 'shared');
|
||||
if (!fs.existsSync(sharedDir)) {
|
||||
fs.mkdirSync(sharedDir, { recursive: true, mode: 0o755 });
|
||||
console.log('[OK] Created directory: ~/.ccs/shared/');
|
||||
}
|
||||
|
||||
// Create shared subdirectories
|
||||
const sharedSubdirs = ['commands', 'skills', 'agents'];
|
||||
for (const subdir of sharedSubdirs) {
|
||||
const subdirPath = path.join(sharedDir, subdir);
|
||||
if (!fs.existsSync(subdirPath)) {
|
||||
fs.mkdirSync(subdirPath, { recursive: true, mode: 0o755 });
|
||||
console.log(`[OK] Created directory: ~/.ccs/shared/${subdir}/`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create config.json if missing
|
||||
const configPath = path.join(ccsDir, 'config.json');
|
||||
if (!fs.existsSync(configPath)) {
|
||||
|
||||
Reference in new issue
Block a user