@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 +48 -0
- package/VERSION +1 -1
- package/bin/ccs.js +10 -0
- package/bin/instance-manager.js +9 -20
- package/bin/shared-manager.js +224 -0
- package/lib/ccs +83 -15
- package/lib/ccs.ps1 +106 -19
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -0
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.
|
|
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');
|
package/bin/instance-manager.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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.
|
|
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:
|
|
71
|
-
echo -e " Profiles:
|
|
72
|
-
echo -e "
|
|
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
|
|
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.
|
|
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:
|
|
127
|
-
Write-Host " Profiles:
|
|
128
|
-
Write-Host "
|
|
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'
|
|
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
package/scripts/postinstall.js
CHANGED
|
@@ -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)) {
|