@kaitranntt/ccs 3.1.1 → 3.2.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/VERSION CHANGED
@@ -1 +1 @@
1
- 3.1.1
1
+ 3.2.0
@@ -6,33 +6,119 @@ const os = require('os');
6
6
 
7
7
  /**
8
8
  * SharedManager - Manages symlinked shared directories for CCS
9
- * Phase 1: Shared Global Data via Symlinks
9
+ * v3.2.0: Symlink-based architecture
10
10
  *
11
- * Purpose: Eliminates duplication of commands/skills across profile instances
12
- * by symlinking to a single ~/.ccs/shared/ directory.
11
+ * Purpose: Eliminates duplication by symlinking:
12
+ * ~/.claude/ ~/.ccs/shared/ ← instance/
13
13
  */
14
14
  class SharedManager {
15
15
  constructor() {
16
16
  this.homeDir = os.homedir();
17
17
  this.sharedDir = path.join(this.homeDir, '.ccs', 'shared');
18
+ this.claudeDir = path.join(this.homeDir, '.claude');
18
19
  this.instancesDir = path.join(this.homeDir, '.ccs', 'instances');
19
20
  this.sharedDirs = ['commands', 'skills', 'agents'];
20
21
  }
21
22
 
22
23
  /**
23
- * Ensure shared directories exist
24
+ * Detect circular symlink before creation
25
+ * @param {string} target - Target path to link to
26
+ * @param {string} linkPath - Path where symlink will be created
27
+ * @returns {boolean} True if circular
28
+ * @private
29
+ */
30
+ _detectCircularSymlink(target, linkPath) {
31
+ // Check if target exists and is symlink
32
+ if (!fs.existsSync(target)) {
33
+ return false;
34
+ }
35
+
36
+ try {
37
+ const stats = fs.lstatSync(target);
38
+ if (!stats.isSymbolicLink()) {
39
+ return false;
40
+ }
41
+
42
+ // Resolve target's link
43
+ const targetLink = fs.readlinkSync(target);
44
+ const resolvedTarget = path.resolve(path.dirname(target), targetLink);
45
+
46
+ // Check if target points back to our shared dir or link path
47
+ const sharedDir = path.join(this.homeDir, '.ccs', 'shared');
48
+ if (resolvedTarget.startsWith(sharedDir) || resolvedTarget === linkPath) {
49
+ console.log(`[!] Circular symlink detected: ${target} → ${resolvedTarget}`);
50
+ return true;
51
+ }
52
+ } catch (err) {
53
+ // If can't read, assume not circular
54
+ return false;
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * Ensure shared directories exist as symlinks to ~/.claude/
62
+ * Creates ~/.claude/ structure if missing
24
63
  */
25
64
  ensureSharedDirectories() {
65
+ // Create ~/.claude/ if missing
66
+ if (!fs.existsSync(this.claudeDir)) {
67
+ console.log('[i] Creating ~/.claude/ directory structure');
68
+ fs.mkdirSync(this.claudeDir, { recursive: true, mode: 0o700 });
69
+ }
70
+
26
71
  // Create shared directory
27
72
  if (!fs.existsSync(this.sharedDir)) {
28
73
  fs.mkdirSync(this.sharedDir, { recursive: true, mode: 0o700 });
29
74
  }
30
75
 
31
- // Create shared subdirectories
76
+ // Create symlinks ~/.ccs/shared/* → ~/.claude/*
32
77
  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 });
78
+ const claudePath = path.join(this.claudeDir, dir);
79
+ const sharedPath = path.join(this.sharedDir, dir);
80
+
81
+ // Create directory in ~/.claude/ if missing
82
+ if (!fs.existsSync(claudePath)) {
83
+ fs.mkdirSync(claudePath, { recursive: true, mode: 0o700 });
84
+ }
85
+
86
+ // Check for circular symlink
87
+ if (this._detectCircularSymlink(claudePath, sharedPath)) {
88
+ console.log(`[!] Skipping ${dir}: circular symlink detected`);
89
+ continue;
90
+ }
91
+
92
+ // If already a symlink pointing to correct target, skip
93
+ if (fs.existsSync(sharedPath)) {
94
+ try {
95
+ const stats = fs.lstatSync(sharedPath);
96
+ if (stats.isSymbolicLink()) {
97
+ const currentTarget = fs.readlinkSync(sharedPath);
98
+ const resolvedTarget = path.resolve(path.dirname(sharedPath), currentTarget);
99
+ if (resolvedTarget === claudePath) {
100
+ continue; // Already correct
101
+ }
102
+ }
103
+ } catch (err) {
104
+ // Continue to recreate
105
+ }
106
+
107
+ // Remove existing directory/link
108
+ fs.rmSync(sharedPath, { recursive: true, force: true });
109
+ }
110
+
111
+ // Create symlink
112
+ try {
113
+ fs.symlinkSync(claudePath, sharedPath, 'dir');
114
+ } catch (err) {
115
+ // Windows fallback: copy directory
116
+ if (process.platform === 'win32') {
117
+ this._copyDirectoryFallback(claudePath, sharedPath);
118
+ console.log(`[!] Symlink failed for ${dir}, copied instead (enable Developer Mode)`);
119
+ } else {
120
+ throw err;
121
+ }
36
122
  }
37
123
  }
38
124
  }
@@ -57,9 +143,9 @@ class SharedManager {
57
143
  try {
58
144
  fs.symlinkSync(targetPath, linkPath, 'dir');
59
145
  } catch (err) {
60
- // Windows fallback: copy directory if symlink fails
146
+ // Windows fallback
61
147
  if (process.platform === 'win32') {
62
- this._copyDirectory(targetPath, linkPath);
148
+ this._copyDirectoryFallback(targetPath, linkPath);
63
149
  console.log(`[!] Symlink failed for ${dir}, copied instead (enable Developer Mode)`);
64
150
  } else {
65
151
  throw err;
@@ -69,155 +155,130 @@ class SharedManager {
69
155
  }
70
156
 
71
157
  /**
72
- * Check if migration is needed
73
- * @returns {boolean}
74
- * @private
158
+ * Migrate from v3.1.1 (copied data in ~/.ccs/shared/) to v3.2.0 (symlinks to ~/.claude/)
159
+ * Runs once on upgrade
75
160
  */
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;
161
+ migrateFromV311() {
162
+ // Check if migration already done (shared dirs are symlinks)
163
+ const commandsPath = path.join(this.sharedDir, 'commands');
164
+ if (fs.existsSync(commandsPath)) {
86
165
  try {
87
- const files = fs.readdirSync(dirPath);
88
- return files.length === 0;
166
+ if (fs.lstatSync(commandsPath).isSymbolicLink()) {
167
+ return; // Already migrated
168
+ }
89
169
  } catch (err) {
90
- return true; // If can't read, assume empty
170
+ // Continue with migration
91
171
  }
92
- });
93
-
94
- return allEmpty;
95
- }
172
+ }
96
173
 
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');
174
+ console.log('[i] Migrating from v3.1.1 to v3.2.0...');
105
175
 
106
- if (!fs.existsSync(claudeDir)) {
107
- return stats; // No content to migrate
176
+ // Ensure ~/.claude/ exists
177
+ if (!fs.existsSync(this.claudeDir)) {
178
+ fs.mkdirSync(this.claudeDir, { recursive: true, mode: 0o700 });
108
179
  }
109
180
 
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
- }
181
+ // Copy user modifications from ~/.ccs/shared/ to ~/.claude/
182
+ for (const dir of this.sharedDirs) {
183
+ const sharedPath = path.join(this.sharedDir, dir);
184
+ const claudePath = path.join(this.claudeDir, dir);
116
185
 
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
- }
186
+ if (!fs.existsSync(sharedPath)) continue;
123
187
 
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
- }
188
+ try {
189
+ const stats = fs.lstatSync(sharedPath);
190
+ if (!stats.isDirectory()) continue;
191
+ } catch (err) {
192
+ continue;
193
+ }
130
194
 
131
- return stats;
132
- }
195
+ // Create claude dir if missing
196
+ if (!fs.existsSync(claudePath)) {
197
+ fs.mkdirSync(claudePath, { recursive: true, mode: 0o700 });
198
+ }
133
199
 
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...');
200
+ // Copy files from shared to claude (preserve user modifications)
201
+ try {
202
+ const entries = fs.readdirSync(sharedPath, { withFileTypes: true });
203
+ let copied = 0;
204
+
205
+ for (const entry of entries) {
206
+ const src = path.join(sharedPath, entry.name);
207
+ const dest = path.join(claudePath, entry.name);
208
+
209
+ // Skip if already exists in claude
210
+ if (fs.existsSync(dest)) continue;
211
+
212
+ if (entry.isDirectory()) {
213
+ fs.cpSync(src, dest, { recursive: true });
214
+ } else {
215
+ fs.copyFileSync(src, dest);
216
+ }
217
+ copied++;
218
+ }
140
219
 
141
- // Check if migration is needed
142
- if (!this._needsMigration()) {
143
- console.log('[OK] Migration not needed (shared dirs have content)');
144
- return;
220
+ if (copied > 0) {
221
+ console.log(`[OK] Migrated ${copied} ${dir} to ~/.claude/${dir}`);
222
+ }
223
+ } catch (err) {
224
+ console.log(`[!] Failed to migrate ${dir}: ${err.message}`);
225
+ }
145
226
  }
146
227
 
147
- console.log('[i] Migrating ~/.claude/ content to ~/.ccs/shared/...');
148
-
149
- // Create shared directories
228
+ // Now run ensureSharedDirectories to create symlinks
150
229
  this.ensureSharedDirectories();
151
230
 
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
231
+ // Update all instances to use new symlinks
168
232
  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);
233
+ try {
234
+ const instances = fs.readdirSync(this.instancesDir);
235
+
236
+ for (const instance of instances) {
237
+ const instancePath = path.join(this.instancesDir, instance);
238
+ try {
239
+ if (fs.statSync(instancePath).isDirectory()) {
240
+ this.linkSharedDirectories(instancePath);
241
+ }
242
+ } catch (err) {
243
+ console.log(`[!] Failed to update instance ${instance}: ${err.message}`);
244
+ }
175
245
  }
246
+ } catch (err) {
247
+ // No instances to update
176
248
  }
177
249
  }
250
+
251
+ console.log('[OK] Migration to v3.2.0 complete');
178
252
  }
179
253
 
180
254
  /**
181
- * Copy directory recursively (SAFE: preserves existing files)
255
+ * Copy directory as fallback (Windows without Developer Mode)
182
256
  * @param {string} src - Source directory
183
257
  * @param {string} dest - Destination directory
184
- * @returns {object} { copied: N, skipped: N }
185
258
  * @private
186
259
  */
187
- _copyDirectory(src, dest) {
260
+ _copyDirectoryFallback(src, dest) {
188
261
  if (!fs.existsSync(src)) {
189
- return { copied: 0, skipped: 0 };
262
+ fs.mkdirSync(src, { recursive: true, mode: 0o700 });
263
+ return;
190
264
  }
191
265
 
192
266
  if (!fs.existsSync(dest)) {
193
- fs.mkdirSync(dest, { recursive: true });
267
+ fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
194
268
  }
195
269
 
196
270
  const entries = fs.readdirSync(src, { withFileTypes: true });
197
- let copied = 0;
198
- let skipped = 0;
199
271
 
200
272
  for (const entry of entries) {
201
273
  const srcPath = path.join(src, entry.name);
202
274
  const destPath = path.join(dest, entry.name);
203
275
 
204
- // SAFETY: Skip if destination exists (preserve user modifications)
205
- if (fs.existsSync(destPath)) {
206
- skipped++;
207
- continue;
208
- }
209
-
210
276
  if (entry.isDirectory()) {
211
- const stats = this._copyDirectory(srcPath, destPath);
212
- copied += stats.copied;
213
- skipped += stats.skipped;
277
+ this._copyDirectoryFallback(srcPath, destPath);
214
278
  } else {
215
279
  fs.copyFileSync(srcPath, destPath);
216
- copied++;
217
280
  }
218
281
  }
219
-
220
- return { copied, skipped };
221
282
  }
222
283
  }
223
284
 
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.1.1"
5
+ CCS_VERSION="3.2.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"
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.1.1"
15
+ $CcsVersion = "3.2.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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaitranntt/ccs",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
5
5
  "keywords": [
6
6
  "cli",
@@ -89,15 +89,16 @@ function createConfigFiles() {
89
89
  }
90
90
  }
91
91
 
92
- // Migrate from ~/.claude/ to ~/.ccs/shared/ (v3.1.1)
92
+ // Migrate from v3.1.1 to v3.2.0 (symlink architecture)
93
93
  console.log('');
94
94
  try {
95
95
  const SharedManager = require('../bin/shared-manager');
96
96
  const sharedManager = new SharedManager();
97
- sharedManager.migrateToSharedStructure();
97
+ sharedManager.migrateFromV311();
98
+ sharedManager.ensureSharedDirectories();
98
99
  } catch (err) {
99
100
  console.warn('[!] Migration warning:', err.message);
100
- console.warn(' You can manually copy files from ~/.claude/ to ~/.ccs/shared/');
101
+ console.warn(' Migration will retry on next run');
101
102
  }
102
103
  console.log('');
103
104