@kaitranntt/ccs 3.0.2 → 3.1.0
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 +15 -0
- package/bin/instance-manager.js +9 -20
- package/bin/shared-manager.js +164 -0
- package/lib/ccs +86 -15
- package/lib/ccs.ps1 +109 -19
- package/package.json +1 -1
- package/scripts/postinstall.js +17 -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.0
|
|
1
|
+
3.1.0
|
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');
|
|
@@ -252,6 +262,11 @@ async function main() {
|
|
|
252
262
|
recovery.showRecoveryHints();
|
|
253
263
|
}
|
|
254
264
|
|
|
265
|
+
// Run migration to shared structure (Phase 1: idempotent)
|
|
266
|
+
const SharedManager = require('./shared-manager');
|
|
267
|
+
const sharedManager = new SharedManager();
|
|
268
|
+
sharedManager.migrateToSharedStructure();
|
|
269
|
+
|
|
255
270
|
// Detect profile
|
|
256
271
|
const { profile, remainingArgs } = detectProfile(args);
|
|
257
272
|
|
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,164 @@
|
|
|
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
|
+
* Migrate existing instances to shared structure
|
|
73
|
+
* Idempotent: Safe to run multiple times
|
|
74
|
+
*/
|
|
75
|
+
migrateToSharedStructure() {
|
|
76
|
+
// Check if migration is needed (shared dirs exist but are empty)
|
|
77
|
+
const needsMigration = !fs.existsSync(this.sharedDir) ||
|
|
78
|
+
this.sharedDirs.every(dir => {
|
|
79
|
+
const dirPath = path.join(this.sharedDir, dir);
|
|
80
|
+
if (!fs.existsSync(dirPath)) return true;
|
|
81
|
+
try {
|
|
82
|
+
const files = fs.readdirSync(dirPath);
|
|
83
|
+
return files.length === 0; // Empty directory needs migration
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return true; // If we can't read it, assume it needs migration
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!needsMigration) {
|
|
90
|
+
return; // Already migrated with content
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create shared directories
|
|
94
|
+
this.ensureSharedDirectories();
|
|
95
|
+
|
|
96
|
+
// Copy from ~/.claude/ (actual Claude CLI directory)
|
|
97
|
+
const claudeDir = path.join(this.homeDir, '.claude');
|
|
98
|
+
|
|
99
|
+
if (fs.existsSync(claudeDir)) {
|
|
100
|
+
// Copy commands to shared (if exists)
|
|
101
|
+
const commandsPath = path.join(claudeDir, 'commands');
|
|
102
|
+
if (fs.existsSync(commandsPath)) {
|
|
103
|
+
this._copyDirectory(commandsPath, path.join(this.sharedDir, 'commands'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Copy skills to shared (if exists)
|
|
107
|
+
const skillsPath = path.join(claudeDir, 'skills');
|
|
108
|
+
if (fs.existsSync(skillsPath)) {
|
|
109
|
+
this._copyDirectory(skillsPath, path.join(this.sharedDir, 'skills'));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Copy agents to shared (if exists)
|
|
113
|
+
const agentsPath = path.join(claudeDir, 'agents');
|
|
114
|
+
if (fs.existsSync(agentsPath)) {
|
|
115
|
+
this._copyDirectory(agentsPath, path.join(this.sharedDir, 'agents'));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Update all instances to use symlinks
|
|
120
|
+
if (fs.existsSync(this.instancesDir)) {
|
|
121
|
+
const instances = fs.readdirSync(this.instancesDir);
|
|
122
|
+
|
|
123
|
+
for (const instance of instances) {
|
|
124
|
+
const instancePath = path.join(this.instancesDir, instance);
|
|
125
|
+
if (fs.statSync(instancePath).isDirectory()) {
|
|
126
|
+
this.linkSharedDirectories(instancePath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('[OK] Migrated to shared structure');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Copy directory recursively (fallback for Windows)
|
|
136
|
+
* @param {string} src - Source directory
|
|
137
|
+
* @param {string} dest - Destination directory
|
|
138
|
+
* @private
|
|
139
|
+
*/
|
|
140
|
+
_copyDirectory(src, dest) {
|
|
141
|
+
if (!fs.existsSync(src)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!fs.existsSync(dest)) {
|
|
146
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
150
|
+
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const srcPath = path.join(src, entry.name);
|
|
153
|
+
const destPath = path.join(dest, entry.name);
|
|
154
|
+
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
this._copyDirectory(srcPath, destPath);
|
|
157
|
+
} else {
|
|
158
|
+
fs.copyFileSync(srcPath, destPath);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
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
|
|
5
|
+
CCS_VERSION="3.1.0"
|
|
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)
|
|
@@ -942,6 +1010,9 @@ auto_recover || {
|
|
|
942
1010
|
exit 1
|
|
943
1011
|
}
|
|
944
1012
|
|
|
1013
|
+
# Run migration to shared structure (Phase 1: idempotent)
|
|
1014
|
+
migrate_to_shared_structure
|
|
1015
|
+
|
|
945
1016
|
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
|
|
946
1017
|
if [[ $# -eq 0 ]] || [[ "${1}" =~ ^- ]]; then
|
|
947
1018
|
# No args or first arg is a flag → use default profile
|
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
|
|
15
|
+
$CcsVersion = "3.1.0"
|
|
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
|
}
|
|
@@ -913,6 +1000,9 @@ if (-not (Invoke-AutoRecovery)) {
|
|
|
913
1000
|
exit 1
|
|
914
1001
|
}
|
|
915
1002
|
|
|
1003
|
+
# Run migration to shared structure (Phase 1: idempotent)
|
|
1004
|
+
Migrate-SharedStructure
|
|
1005
|
+
|
|
916
1006
|
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
|
|
917
1007
|
if ($RemainingArgs.Count -eq 0 -or $RemainingArgs[0] -match '^-') {
|
|
918
1008
|
# No args or first arg is a flag → use default profile
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -72,6 +72,23 @@ 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
|
+
|
|
75
92
|
// Create config.json if missing
|
|
76
93
|
const configPath = path.join(ccsDir, 'config.json');
|
|
77
94
|
if (!fs.existsSync(configPath)) {
|