mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-18 14:13:21 +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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user