From d925dfc169c817f48f07b04d9f508042aa65a3b9 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 10 Nov 2025 20:45:21 -0500 Subject: [PATCH] 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 --- CHANGELOG.md | 27 ++ CLAUDE.md | 443 ++++++++++----------- README.md | 48 +++ VERSION | 2 +- bin/ccs.js | 15 + bin/instance-manager.js | 29 +- bin/shared-manager.js | 164 ++++++++ docs/session-sharing-technical-analysis.md | 239 +++++++++++ docs/testing-requirements.md | 418 ------------------- installers/install.ps1 | 2 +- installers/install.sh | 2 +- lib/ccs | 101 ++++- lib/ccs.ps1 | 128 +++++- package.json | 2 +- scripts/postinstall.js | 17 + 15 files changed, 929 insertions(+), 708 deletions(-) create mode 100644 bin/shared-manager.js create mode 100644 docs/session-sharing-technical-analysis.md delete mode 100644 docs/testing-requirements.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fc4e064..42253282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 0b69e40c..a09af483 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `. No credential copying, no vault encryption. +**Login-Per-Profile**: Each profile = isolated Claude instance via `ccs auth create `. 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 ` → 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 ` → 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` -6. Push to trigger GitHub Actions: `git push origin main && git push origin v` +### 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 && git push origin main && git push origin v` -## 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 diff --git a/README.md b/README.md index 6e13cd31..adebaea0 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/VERSION b/VERSION index b5021469..fd2a0186 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.2 +3.1.0 diff --git a/bin/ccs.js b/bin/ccs.js index 9e843973..ac99ec11 100755 --- a/bin/ccs.js +++ b/bin/ccs.js @@ -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); diff --git a/bin/instance-manager.js b/bin/instance-manager.js index 2eab91cc..d317b0c3 100644 --- a/bin/instance-manager.js +++ b/bin/instance-manager.js @@ -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); - } } /** diff --git a/bin/shared-manager.js b/bin/shared-manager.js new file mode 100644 index 00000000..caa2ff5c --- /dev/null +++ b/bin/shared-manager.js @@ -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; diff --git a/docs/session-sharing-technical-analysis.md b/docs/session-sharing-technical-analysis.md new file mode 100644 index 00000000..2798f4e8 --- /dev/null +++ b/docs/session-sharing-technical-analysis.md @@ -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/ +│ └── / +│ └── .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":"","message":{"role":"user","content":"..."},"uuid":"","timestamp":"..."} +{"type":"assistant","sessionId":"","message":{"role":"assistant","content":[...]},"requestId":"req_...","uuid":"","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 ` +2. CLI reads `~/.claude/projects//.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//.jsonl` +2. Generate new UUID: `new_id=$(uuidgen | tr '[:upper:]' '[:lower:]')` +3. Replace `sessionId` in JSONL: `sed 's///g' file.jsonl > new.jsonl` +4. Place in personal profile: `~/.ccs/instances/personal/.claude/projects//.jsonl` +5. Resume: `ccs personal` → `claude --resume ` + +**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 --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 ` + +**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. diff --git a/docs/testing-requirements.md b/docs/testing-requirements.md deleted file mode 100644 index c96e9453..00000000 --- a/docs/testing-requirements.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/installers/install.ps1 b/installers/install.ps1 index 58990428..19d8f5c1 100644 --- a/installers/install.ps1 +++ b/installers/install.ps1 @@ -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) { diff --git a/installers/install.sh b/installers/install.sh index 9d1d15ea..19f83e3a 100755 --- a/installers/install.sh +++ b/installers/install.sh @@ -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 diff --git a/lib/ccs b/lib/ccs index 460991e3..aa375aba 100755 --- a/lib/ccs +++ b/lib/ccs @@ -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 diff --git a/lib/ccs.ps1 b/lib/ccs.ps1 index bb43978a..5b5694f1 100644 --- a/lib/ccs.ps1 +++ b/lib/ccs.ps1 @@ -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 diff --git a/package.json b/package.json index 728c8055..fcff3c24 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/postinstall.js b/scripts/postinstall.js index ff41be1f..1a91a39e 100755 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -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)) {