@kaitranntt/ccs 3.0.2 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,6 +92,16 @@ $env:CCS_CLAUDE_PATH = "D:\Tools\Claude\claude.exe" # Windows
92
92
 
93
93
  **See [Troubleshooting Guide](./docs/en/troubleshooting.md#claude-cli-in-non-standard-location) for detailed setup instructions.**
94
94
 
95
+ ### Windows Symlink Support (Developer Mode)
96
+
97
+ **Windows users**: Enable Developer Mode for true symlinks (better performance, instant sync):
98
+
99
+ 1. Open **Settings** → **Privacy & Security** → **For developers**
100
+ 2. Enable **Developer Mode**
101
+ 3. Reinstall CCS: `npm install -g @kaitranntt/ccs`
102
+
103
+ **Without Developer Mode**: CCS automatically falls back to copying directories (works but no instant sync across profiles).
104
+
95
105
  ---
96
106
 
97
107
  ### Your First Switch
@@ -153,6 +163,44 @@ ccs work-2 # Switch to second company account
153
163
 
154
164
  ---
155
165
 
166
+ ## 📁 Shared Data Architecture
167
+
168
+ **v3.1 Shared Global Data**: Commands and skills are symlinked across all profiles via `~/.ccs/shared/`, eliminating duplication.
169
+
170
+ **Directory Structure**:
171
+ ```
172
+ ~/.ccs/
173
+ ├── shared/ # Shared across all profiles
174
+ │ ├── commands/ # Custom slash commands
175
+ │ └── skills/ # Claude Code skills
176
+ ├── instances/ # Profile-specific data
177
+ │ ├── work/
178
+ │ │ ├── commands@ → ~/.ccs/shared/commands/ # Symlink
179
+ │ │ ├── skills@ → ~/.ccs/shared/skills/ # Symlink
180
+ │ │ ├── settings.json # Profile-specific config
181
+ │ │ └── sessions/ # Profile-specific sessions
182
+ │ └── personal/
183
+ │ └── ...
184
+ ```
185
+
186
+ **Benefits**:
187
+ - No duplication of commands/skills across profiles
188
+ - Single source of truth for shared resources
189
+ - Automatic migration from v3.0 (runs on first use)
190
+ - Windows fallback: copies if symlinks unavailable (enable Developer Mode for true symlinks)
191
+
192
+ **What's Shared**:
193
+ - `.claude/commands/` - Custom slash commands
194
+ - `.claude/skills/` - Claude Code skills
195
+
196
+ **What's Profile-Specific**:
197
+ - `settings.json` - API keys, credentials
198
+ - `sessions/` - Conversation history
199
+ - `todolists/` - Task tracking
200
+ - `logs/` - Profile-specific logs
201
+
202
+ ---
203
+
156
204
  ## 🏗️ Architecture Overview
157
205
 
158
206
  **v3.0 Login-Per-Profile Model**: Each profile is an isolated Claude instance where users login directly. No credential copying or vault encryption.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.2
1
+ 3.1.1
package/bin/ccs.js CHANGED
@@ -133,10 +133,20 @@ function handleHelpCommand() {
133
133
  // Configuration
134
134
  console.log(colored('Configuration:', 'cyan'));
135
135
  console.log(' Config File: ~/.ccs/config.json');
136
+ console.log(' Profiles: ~/.ccs/profiles.json');
137
+ console.log(' Instances: ~/.ccs/instances/');
136
138
  console.log(' Settings: ~/.ccs/*.settings.json');
137
139
  console.log(' Environment: CCS_CONFIG (override config path)');
138
140
  console.log('');
139
141
 
142
+ // Shared Data
143
+ console.log(colored('Shared Data:', 'cyan'));
144
+ console.log(' Commands: ~/.ccs/shared/commands/');
145
+ console.log(' Skills: ~/.ccs/shared/skills/');
146
+ console.log(' Agents: ~/.ccs/shared/agents/');
147
+ console.log(' Note: Commands, skills, and agents are symlinked across all profiles');
148
+ console.log('');
149
+
140
150
  // Uninstall
141
151
  console.log(colored('Uninstall:', 'yellow'));
142
152
  console.log(' npm: npm uninstall -g @kaitranntt/ccs');
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const SharedManager = require('./shared-manager');
6
7
 
7
8
  /**
8
9
  * Instance Manager (Simplified)
@@ -14,6 +15,7 @@ const os = require('os');
14
15
  class InstanceManager {
15
16
  constructor() {
16
17
  this.instancesDir = path.join(os.homedir(), '.ccs', 'instances');
18
+ this.sharedManager = new SharedManager();
17
19
  }
18
20
 
19
21
  /**
@@ -56,7 +58,7 @@ class InstanceManager {
56
58
  // Create base directory
57
59
  fs.mkdirSync(instancePath, { recursive: true, mode: 0o700 });
58
60
 
59
- // Create Claude-expected subdirectories
61
+ // Create Claude-expected subdirectories (profile-specific only)
60
62
  const subdirs = [
61
63
  'session-env',
62
64
  'todos',
@@ -64,9 +66,7 @@ class InstanceManager {
64
66
  'file-history',
65
67
  'shell-snapshots',
66
68
  'debug',
67
- '.anthropic',
68
- 'commands',
69
- 'skills'
69
+ '.anthropic'
70
70
  ];
71
71
 
72
72
  subdirs.forEach(dir => {
@@ -76,7 +76,10 @@ class InstanceManager {
76
76
  }
77
77
  });
78
78
 
79
- // Copy global configs if exist
79
+ // Symlink shared directories (Phase 1: commands, skills)
80
+ this.sharedManager.linkSharedDirectories(instancePath);
81
+
82
+ // Copy global configs if exist (settings.json only)
80
83
  this._copyGlobalConfigs(instancePath);
81
84
  } catch (error) {
82
85
  throw new Error(`Failed to initialize instance for ${profileName}: ${error.message}`);
@@ -158,26 +161,12 @@ class InstanceManager {
158
161
  _copyGlobalConfigs(instancePath) {
159
162
  const globalConfigDir = path.join(os.homedir(), '.claude');
160
163
 
161
- // Copy settings.json if exists
164
+ // Copy settings.json only (commands/skills are now symlinked to shared/)
162
165
  const globalSettings = path.join(globalConfigDir, 'settings.json');
163
166
  if (fs.existsSync(globalSettings)) {
164
167
  const instanceSettings = path.join(instancePath, 'settings.json');
165
168
  fs.copyFileSync(globalSettings, instanceSettings);
166
169
  }
167
-
168
- // Copy commands directory if exists
169
- const globalCommands = path.join(globalConfigDir, 'commands');
170
- if (fs.existsSync(globalCommands)) {
171
- const instanceCommands = path.join(instancePath, 'commands');
172
- this._copyDirectory(globalCommands, instanceCommands);
173
- }
174
-
175
- // Copy skills directory if exists
176
- const globalSkills = path.join(globalConfigDir, 'skills');
177
- if (fs.existsSync(globalSkills)) {
178
- const instanceSkills = path.join(instancePath, 'skills');
179
- this._copyDirectory(globalSkills, instanceSkills);
180
- }
181
170
  }
182
171
 
183
172
  /**
@@ -0,0 +1,224 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ /**
8
+ * SharedManager - Manages symlinked shared directories for CCS
9
+ * Phase 1: Shared Global Data via Symlinks
10
+ *
11
+ * Purpose: Eliminates duplication of commands/skills across profile instances
12
+ * by symlinking to a single ~/.ccs/shared/ directory.
13
+ */
14
+ class SharedManager {
15
+ constructor() {
16
+ this.homeDir = os.homedir();
17
+ this.sharedDir = path.join(this.homeDir, '.ccs', 'shared');
18
+ this.instancesDir = path.join(this.homeDir, '.ccs', 'instances');
19
+ this.sharedDirs = ['commands', 'skills', 'agents'];
20
+ }
21
+
22
+ /**
23
+ * Ensure shared directories exist
24
+ */
25
+ ensureSharedDirectories() {
26
+ // Create shared directory
27
+ if (!fs.existsSync(this.sharedDir)) {
28
+ fs.mkdirSync(this.sharedDir, { recursive: true, mode: 0o700 });
29
+ }
30
+
31
+ // Create shared subdirectories
32
+ for (const dir of this.sharedDirs) {
33
+ const dirPath = path.join(this.sharedDir, dir);
34
+ if (!fs.existsSync(dirPath)) {
35
+ fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Link shared directories to instance
42
+ * @param {string} instancePath - Path to instance directory
43
+ */
44
+ linkSharedDirectories(instancePath) {
45
+ this.ensureSharedDirectories();
46
+
47
+ for (const dir of this.sharedDirs) {
48
+ const linkPath = path.join(instancePath, dir);
49
+ const targetPath = path.join(this.sharedDir, dir);
50
+
51
+ // Remove existing directory/link
52
+ if (fs.existsSync(linkPath)) {
53
+ fs.rmSync(linkPath, { recursive: true, force: true });
54
+ }
55
+
56
+ // Create symlink
57
+ try {
58
+ fs.symlinkSync(targetPath, linkPath, 'dir');
59
+ } catch (err) {
60
+ // Windows fallback: copy directory if symlink fails
61
+ if (process.platform === 'win32') {
62
+ this._copyDirectory(targetPath, linkPath);
63
+ console.log(`[!] Symlink failed for ${dir}, copied instead (enable Developer Mode)`);
64
+ } else {
65
+ throw err;
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if migration is needed
73
+ * @returns {boolean}
74
+ * @private
75
+ */
76
+ _needsMigration() {
77
+ // If shared dir doesn't exist, migration needed
78
+ if (!fs.existsSync(this.sharedDir)) {
79
+ return true;
80
+ }
81
+
82
+ // Check if ALL shared directories are empty
83
+ const allEmpty = this.sharedDirs.every(dir => {
84
+ const dirPath = path.join(this.sharedDir, dir);
85
+ if (!fs.existsSync(dirPath)) return true;
86
+ try {
87
+ const files = fs.readdirSync(dirPath);
88
+ return files.length === 0;
89
+ } catch (err) {
90
+ return true; // If can't read, assume empty
91
+ }
92
+ });
93
+
94
+ return allEmpty;
95
+ }
96
+
97
+ /**
98
+ * Perform migration from ~/.claude/ to ~/.ccs/shared/
99
+ * @returns {object} { commands: N, skills: N, agents: N }
100
+ * @private
101
+ */
102
+ _performMigration() {
103
+ const stats = { commands: 0, skills: 0, agents: 0 };
104
+ const claudeDir = path.join(this.homeDir, '.claude');
105
+
106
+ if (!fs.existsSync(claudeDir)) {
107
+ return stats; // No content to migrate
108
+ }
109
+
110
+ // Migrate commands
111
+ const commandsPath = path.join(claudeDir, 'commands');
112
+ if (fs.existsSync(commandsPath)) {
113
+ const result = this._copyDirectory(commandsPath, path.join(this.sharedDir, 'commands'));
114
+ stats.commands = result.copied;
115
+ }
116
+
117
+ // Migrate skills
118
+ const skillsPath = path.join(claudeDir, 'skills');
119
+ if (fs.existsSync(skillsPath)) {
120
+ const result = this._copyDirectory(skillsPath, path.join(this.sharedDir, 'skills'));
121
+ stats.skills = result.copied;
122
+ }
123
+
124
+ // Migrate agents
125
+ const agentsPath = path.join(claudeDir, 'agents');
126
+ if (fs.existsSync(agentsPath)) {
127
+ const result = this._copyDirectory(agentsPath, path.join(this.sharedDir, 'agents'));
128
+ stats.agents = result.copied;
129
+ }
130
+
131
+ return stats;
132
+ }
133
+
134
+ /**
135
+ * Migrate existing instances to shared structure
136
+ * Idempotent: Safe to run multiple times
137
+ */
138
+ migrateToSharedStructure() {
139
+ console.log('[i] Checking for content migration...');
140
+
141
+ // Check if migration is needed
142
+ if (!this._needsMigration()) {
143
+ console.log('[OK] Migration not needed (shared dirs have content)');
144
+ return;
145
+ }
146
+
147
+ console.log('[i] Migrating ~/.claude/ content to ~/.ccs/shared/...');
148
+
149
+ // Create shared directories
150
+ this.ensureSharedDirectories();
151
+
152
+ // Perform migration
153
+ const stats = this._performMigration();
154
+
155
+ // Show results
156
+ const total = stats.commands + stats.skills + stats.agents;
157
+ if (total === 0) {
158
+ console.log('[OK] No content to migrate (empty ~/.claude/)');
159
+ } else {
160
+ const parts = [];
161
+ if (stats.commands > 0) parts.push(`${stats.commands} commands`);
162
+ if (stats.skills > 0) parts.push(`${stats.skills} skills`);
163
+ if (stats.agents > 0) parts.push(`${stats.agents} agents`);
164
+ console.log(`[OK] Migrated ${parts.join(', ')}`);
165
+ }
166
+
167
+ // Update all instances to use symlinks
168
+ if (fs.existsSync(this.instancesDir)) {
169
+ const instances = fs.readdirSync(this.instancesDir);
170
+
171
+ for (const instance of instances) {
172
+ const instancePath = path.join(this.instancesDir, instance);
173
+ if (fs.statSync(instancePath).isDirectory()) {
174
+ this.linkSharedDirectories(instancePath);
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Copy directory recursively (SAFE: preserves existing files)
182
+ * @param {string} src - Source directory
183
+ * @param {string} dest - Destination directory
184
+ * @returns {object} { copied: N, skipped: N }
185
+ * @private
186
+ */
187
+ _copyDirectory(src, dest) {
188
+ if (!fs.existsSync(src)) {
189
+ return { copied: 0, skipped: 0 };
190
+ }
191
+
192
+ if (!fs.existsSync(dest)) {
193
+ fs.mkdirSync(dest, { recursive: true });
194
+ }
195
+
196
+ const entries = fs.readdirSync(src, { withFileTypes: true });
197
+ let copied = 0;
198
+ let skipped = 0;
199
+
200
+ for (const entry of entries) {
201
+ const srcPath = path.join(src, entry.name);
202
+ const destPath = path.join(dest, entry.name);
203
+
204
+ // SAFETY: Skip if destination exists (preserve user modifications)
205
+ if (fs.existsSync(destPath)) {
206
+ skipped++;
207
+ continue;
208
+ }
209
+
210
+ if (entry.isDirectory()) {
211
+ const stats = this._copyDirectory(srcPath, destPath);
212
+ copied += stats.copied;
213
+ skipped += stats.skipped;
214
+ } else {
215
+ fs.copyFileSync(srcPath, destPath);
216
+ copied++;
217
+ }
218
+ }
219
+
220
+ return { copied, skipped };
221
+ }
222
+ }
223
+
224
+ module.exports = SharedManager;
package/lib/ccs CHANGED
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # Version (updated by scripts/bump-version.sh)
5
- CCS_VERSION="3.0.2"
5
+ CCS_VERSION="3.1.1"
6
6
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
7
  readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
8
8
  readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
@@ -67,9 +67,15 @@ show_help() {
67
67
  echo -e " ${YELLOW}-v, --version${RESET} Show version and installation info"
68
68
  echo ""
69
69
  echo -e "${CYAN}Configuration:${RESET}"
70
- echo -e " Config: ~/.ccs/config.json"
71
- echo -e " Profiles: ~/.ccs/profiles.json"
72
- echo -e " Settings: ~/.ccs/*.settings.json"
70
+ echo -e " Config: ~/.ccs/config.json"
71
+ echo -e " Profiles: ~/.ccs/profiles.json"
72
+ echo -e " Instances: ~/.ccs/instances/"
73
+ echo -e " Settings: ~/.ccs/*.settings.json"
74
+ echo ""
75
+ echo -e "${CYAN}Shared Data:${RESET}"
76
+ echo -e " Commands: ~/.ccs/shared/commands/"
77
+ echo -e " Skills: ~/.ccs/shared/skills/"
78
+ echo -e " Note: Commands, skills, and agents are symlinked across all profiles"
73
79
  echo ""
74
80
  echo -e "${CYAN}Documentation:${RESET}"
75
81
  echo -e " GitHub: ${CYAN}https://github.com/kaitranntt/ccs${RESET}"
@@ -493,6 +499,73 @@ sanitize_profile_name() {
493
499
  echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g'
494
500
  }
495
501
 
502
+ # Link shared directories (Phase 1: Shared Global Data)
503
+ link_shared_directories() {
504
+ local instance_path="$1"
505
+ local shared_dir="$HOME/.ccs/shared"
506
+
507
+ # Ensure shared directories exist
508
+ mkdir -p "$shared_dir"/{commands,skills,agents}
509
+
510
+ # Create symlinks (remove existing first if present)
511
+ for dir in commands skills agents; do
512
+ local link_path="$instance_path/$dir"
513
+ local target_path="$shared_dir/$dir"
514
+
515
+ # Remove existing directory/link
516
+ [[ -e "$link_path" ]] && rm -rf "$link_path"
517
+
518
+ # Create symlink
519
+ ln -sf "$target_path" "$link_path"
520
+ done
521
+ }
522
+
523
+ # Migrate to shared structure (Phase 1: Auto-migration)
524
+ migrate_to_shared_structure() {
525
+ local shared_dir="$HOME/.ccs/shared"
526
+
527
+ # Check if migration is needed (shared dirs exist but are empty)
528
+ if [[ -d "$shared_dir" ]]; then
529
+ local needs_migration=false
530
+ for dir in commands skills agents; do
531
+ if [[ ! -d "$shared_dir/$dir" ]] || [[ -z "$(ls -A "$shared_dir/$dir" 2>/dev/null)" ]]; then
532
+ needs_migration=true
533
+ break
534
+ fi
535
+ done
536
+
537
+ [[ "$needs_migration" == "false" ]] && return 0
538
+ fi
539
+
540
+ # Create shared directory
541
+ mkdir -p "$shared_dir"/{commands,skills,agents}
542
+
543
+ # Copy from ~/.claude/ (actual Claude CLI directory)
544
+ local claude_dir="$HOME/.claude"
545
+
546
+ if [[ -d "$claude_dir" ]]; then
547
+ # Copy commands to shared (if exists)
548
+ [[ -d "$claude_dir/commands" ]] && \
549
+ cp -r "$claude_dir/commands"/* "$shared_dir/commands/" 2>/dev/null || true
550
+
551
+ # Copy skills to shared (if exists)
552
+ [[ -d "$claude_dir/skills" ]] && \
553
+ cp -r "$claude_dir/skills"/* "$shared_dir/skills/" 2>/dev/null || true
554
+
555
+ # Copy agents to shared (if exists)
556
+ [[ -d "$claude_dir/agents" ]] && \
557
+ cp -r "$claude_dir/agents"/* "$shared_dir/agents/" 2>/dev/null || true
558
+ fi
559
+
560
+ # Update all instances to use symlinks
561
+ for instance_path in "$INSTANCES_DIR"/*; do
562
+ [[ -d "$instance_path" ]] || continue
563
+ link_shared_directories "$instance_path"
564
+ done
565
+
566
+ echo "[OK] Migrated to shared structure"
567
+ }
568
+
496
569
  # Initialize new instance directory
497
570
  initialize_instance() {
498
571
  local instance_path="$1"
@@ -500,12 +573,15 @@ initialize_instance() {
500
573
  # Create base directory
501
574
  mkdir -m 0700 -p "$instance_path"
502
575
 
503
- # Create subdirectories
504
- local subdirs=(session-env todos logs file-history shell-snapshots debug .anthropic commands skills)
576
+ # Create subdirectories (profile-specific only)
577
+ local subdirs=(session-env todos logs file-history shell-snapshots debug .anthropic)
505
578
  for dir in "${subdirs[@]}"; do
506
579
  mkdir -m 0700 -p "$instance_path/$dir"
507
580
  done
508
581
 
582
+ # Symlink shared directories
583
+ link_shared_directories "$instance_path"
584
+
509
585
  # Copy global configs (optional)
510
586
  copy_global_configs "$instance_path"
511
587
  }
@@ -527,17 +603,9 @@ copy_global_configs() {
527
603
  local instance_path="$1"
528
604
  local global_claude="$HOME/.claude"
529
605
 
530
- # Copy settings.json
606
+ # Copy settings.json only (commands/skills are now symlinked to shared/)
531
607
  [[ -f "$global_claude/settings.json" ]] && \
532
608
  cp "$global_claude/settings.json" "$instance_path/settings.json" 2>/dev/null || true
533
-
534
- # Copy commands/
535
- [[ -d "$global_claude/commands" ]] && \
536
- cp -r "$global_claude/commands" "$instance_path/" 2>/dev/null || true
537
-
538
- # Copy skills/
539
- [[ -d "$global_claude/skills" ]] && \
540
- cp -r "$global_claude/skills" "$instance_path/" 2>/dev/null || true
541
609
  }
542
610
 
543
611
  # Ensure instance exists (lazy initialization)
package/lib/ccs.ps1 CHANGED
@@ -12,7 +12,7 @@ param(
12
12
  $ErrorActionPreference = "Stop"
13
13
 
14
14
  # Version (updated by scripts/bump-version.sh)
15
- $CcsVersion = "3.0.2"
15
+ $CcsVersion = "3.1.1"
16
16
  $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
17
17
  $ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
18
18
  $ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
@@ -123,9 +123,15 @@ function Show-Help {
123
123
  Write-ColorLine " -v, --version Show version and installation info" "Yellow"
124
124
  Write-Host ""
125
125
  Write-ColorLine "Configuration:" "Cyan"
126
- Write-Host " Config: ~/.ccs/config.json"
127
- Write-Host " Profiles: ~/.ccs/profiles.json"
128
- Write-Host " Settings: ~/.ccs/*.settings.json"
126
+ Write-Host " Config: ~/.ccs/config.json"
127
+ Write-Host " Profiles: ~/.ccs/profiles.json"
128
+ Write-Host " Instances: ~/.ccs/instances/"
129
+ Write-Host " Settings: ~/.ccs/*.settings.json"
130
+ Write-Host ""
131
+ Write-ColorLine "Shared Data:" "Cyan"
132
+ Write-Host " Commands: ~/.ccs/shared/commands/"
133
+ Write-Host " Skills: ~/.ccs/shared/skills/"
134
+ Write-Host " Note: Commands, skills, and agents are symlinked across all profiles"
129
135
  Write-Host ""
130
136
  Write-ColorLine "Documentation:" "Cyan"
131
137
  Write-Host " GitHub: https://github.com/kaitranntt/ccs"
@@ -422,28 +428,106 @@ function Set-InstancePermissions {
422
428
  Set-Acl -Path $Path -AclObject $Acl
423
429
  }
424
430
 
431
+ function Link-SharedDirectories {
432
+ param([string]$InstancePath)
433
+
434
+ $SharedDir = "$env:USERPROFILE\.ccs\shared"
435
+
436
+ # Ensure shared directories exist
437
+ @('commands', 'skills', 'agents') | ForEach-Object {
438
+ $Dir = Join-Path $SharedDir $_
439
+ if (-not (Test-Path $Dir)) {
440
+ New-Item -ItemType Directory -Path $Dir -Force | Out-Null
441
+ }
442
+ }
443
+
444
+ # Create symlinks (requires Windows Developer Mode or admin)
445
+ @('commands', 'skills', 'agents') | ForEach-Object {
446
+ $LinkPath = Join-Path $InstancePath $_
447
+ $TargetPath = Join-Path $SharedDir $_
448
+
449
+ # Remove existing directory/link
450
+ if (Test-Path $LinkPath) {
451
+ Remove-Item $LinkPath -Recurse -Force
452
+ }
453
+
454
+ # Try creating symlink (requires privileges)
455
+ try {
456
+ New-Item -ItemType SymbolicLink -Path $LinkPath -Target $TargetPath -Force | Out-Null
457
+ } catch {
458
+ # Fallback: Copy directory instead (suboptimal but functional)
459
+ Copy-Item $TargetPath -Destination $LinkPath -Recurse -Force
460
+ Write-Host "[!] Symlink failed for $_, copied instead (enable Developer Mode)" -ForegroundColor Yellow
461
+ }
462
+ }
463
+ }
464
+
465
+ function Migrate-SharedStructure {
466
+ $SharedDir = "$env:USERPROFILE\.ccs\shared"
467
+
468
+ # Check if migration is needed (shared dirs exist but are empty)
469
+ if (Test-Path $SharedDir) {
470
+ $NeedsMigration = $false
471
+ foreach ($Dir in @('commands', 'skills', 'agents')) {
472
+ $DirPath = Join-Path $SharedDir $Dir
473
+ if (-not (Test-Path $DirPath) -or (Get-ChildItem $DirPath -ErrorAction SilentlyContinue).Count -eq 0) {
474
+ $NeedsMigration = $true
475
+ break
476
+ }
477
+ }
478
+
479
+ if (-not $NeedsMigration) { return }
480
+ }
481
+
482
+ # Create shared directory
483
+ @('commands', 'skills', 'agents') | ForEach-Object {
484
+ $Dir = Join-Path $SharedDir $_
485
+ New-Item -ItemType Directory -Path $Dir -Force | Out-Null
486
+ }
487
+
488
+ # Copy from ~/.claude/ (actual Claude CLI directory)
489
+ $ClaudeDir = "$env:USERPROFILE\.claude"
490
+
491
+ if (Test-Path $ClaudeDir) {
492
+ # Copy commands to shared (if exists)
493
+ $CommandsPath = Join-Path $ClaudeDir "commands"
494
+ if (Test-Path $CommandsPath) {
495
+ Copy-Item "$CommandsPath\*" -Destination "$SharedDir\commands\" -Recurse -ErrorAction SilentlyContinue
496
+ }
497
+
498
+ # Copy skills to shared (if exists)
499
+ $SkillsPath = Join-Path $ClaudeDir "skills"
500
+ if (Test-Path $SkillsPath) {
501
+ Copy-Item "$SkillsPath\*" -Destination "$SharedDir\skills\" -Recurse -ErrorAction SilentlyContinue
502
+ }
503
+
504
+ # Copy agents to shared (if exists)
505
+ $AgentsPath = Join-Path $ClaudeDir "agents"
506
+ if (Test-Path $AgentsPath) {
507
+ Copy-Item "$AgentsPath\*" -Destination "$SharedDir\agents\" -Recurse -ErrorAction SilentlyContinue
508
+ }
509
+ }
510
+
511
+ # Update all instances to use symlinks
512
+ if (Test-Path $InstancesDir) {
513
+ Get-ChildItem $InstancesDir -Directory | ForEach-Object {
514
+ Link-SharedDirectories $_.FullName
515
+ }
516
+ }
517
+
518
+ Write-Host "[OK] Migrated to shared structure"
519
+ }
520
+
425
521
  function Copy-GlobalConfigs {
426
522
  param([string]$InstancePath)
427
523
 
428
524
  $GlobalClaude = "$env:USERPROFILE\.claude"
429
525
 
430
- # Copy settings.json
526
+ # Copy settings.json only (commands/skills are now symlinked to shared/)
431
527
  $GlobalSettings = Join-Path $GlobalClaude "settings.json"
432
528
  if (Test-Path $GlobalSettings) {
433
529
  Copy-Item $GlobalSettings -Destination (Join-Path $InstancePath "settings.json") -ErrorAction SilentlyContinue
434
530
  }
435
-
436
- # Copy commands/
437
- $GlobalCommands = Join-Path $GlobalClaude "commands"
438
- if (Test-Path $GlobalCommands) {
439
- Copy-Item $GlobalCommands -Destination $InstancePath -Recurse -ErrorAction SilentlyContinue
440
- }
441
-
442
- # Copy skills/
443
- $GlobalSkills = Join-Path $GlobalClaude "skills"
444
- if (Test-Path $GlobalSkills) {
445
- Copy-Item $GlobalSkills -Destination $InstancePath -Recurse -ErrorAction SilentlyContinue
446
- }
447
531
  }
448
532
 
449
533
  function Initialize-Instance {
@@ -453,15 +537,18 @@ function Initialize-Instance {
453
537
  New-Item -ItemType Directory -Path $InstancePath -Force | Out-Null
454
538
  Set-InstancePermissions $InstancePath
455
539
 
456
- # Create subdirectories
540
+ # Create subdirectories (profile-specific only)
457
541
  $Subdirs = @('session-env', 'todos', 'logs', 'file-history',
458
- 'shell-snapshots', 'debug', '.anthropic', 'commands', 'skills')
542
+ 'shell-snapshots', 'debug', '.anthropic')
459
543
 
460
544
  foreach ($Dir in $Subdirs) {
461
545
  $DirPath = Join-Path $InstancePath $Dir
462
546
  New-Item -ItemType Directory -Path $DirPath -Force | Out-Null
463
547
  }
464
548
 
549
+ # Symlink shared directories
550
+ Link-SharedDirectories $InstancePath
551
+
465
552
  # Copy global configs
466
553
  Copy-GlobalConfigs $InstancePath
467
554
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaitranntt/ccs",
3
- "version": "3.0.2",
3
+ "version": "3.1.1",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
5
5
  "keywords": [
6
6
  "cli",
@@ -72,6 +72,35 @@ function createConfigFiles() {
72
72
  console.log('[OK] Created directory: ~/.ccs/');
73
73
  }
74
74
 
75
+ // Create ~/.ccs/shared/ directory structure (Phase 1)
76
+ const sharedDir = path.join(ccsDir, 'shared');
77
+ if (!fs.existsSync(sharedDir)) {
78
+ fs.mkdirSync(sharedDir, { recursive: true, mode: 0o755 });
79
+ console.log('[OK] Created directory: ~/.ccs/shared/');
80
+ }
81
+
82
+ // Create shared subdirectories
83
+ const sharedSubdirs = ['commands', 'skills', 'agents'];
84
+ for (const subdir of sharedSubdirs) {
85
+ const subdirPath = path.join(sharedDir, subdir);
86
+ if (!fs.existsSync(subdirPath)) {
87
+ fs.mkdirSync(subdirPath, { recursive: true, mode: 0o755 });
88
+ console.log(`[OK] Created directory: ~/.ccs/shared/${subdir}/`);
89
+ }
90
+ }
91
+
92
+ // Migrate from ~/.claude/ to ~/.ccs/shared/ (v3.1.1)
93
+ console.log('');
94
+ try {
95
+ const SharedManager = require('../bin/shared-manager');
96
+ const sharedManager = new SharedManager();
97
+ sharedManager.migrateToSharedStructure();
98
+ } catch (err) {
99
+ console.warn('[!] Migration warning:', err.message);
100
+ console.warn(' You can manually copy files from ~/.claude/ to ~/.ccs/shared/');
101
+ }
102
+ console.log('');
103
+
75
104
  // Create config.json if missing
76
105
  const configPath = path.join(ccsDir, 'config.json');
77
106
  if (!fs.existsSync(configPath)) {