@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 +1 -1
- package/bin/shared-manager.js +175 -114
- package/lib/ccs +1 -1
- package/lib/ccs.ps1 +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +4 -3
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.2.0
|
package/bin/shared-manager.js
CHANGED
|
@@ -6,33 +6,119 @@ const os = require('os');
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* SharedManager - Manages symlinked shared directories for CCS
|
|
9
|
-
*
|
|
9
|
+
* v3.2.0: Symlink-based architecture
|
|
10
10
|
*
|
|
11
|
-
* Purpose: Eliminates duplication
|
|
12
|
-
*
|
|
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
|
-
*
|
|
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
|
|
76
|
+
// Create symlinks ~/.ccs/shared/* → ~/.claude/*
|
|
32
77
|
for (const dir of this.sharedDirs) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
146
|
+
// Windows fallback
|
|
61
147
|
if (process.platform === 'win32') {
|
|
62
|
-
this.
|
|
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
|
-
*
|
|
73
|
-
*
|
|
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
|
-
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
88
|
-
|
|
166
|
+
if (fs.lstatSync(commandsPath).isSymbolicLink()) {
|
|
167
|
+
return; // Already migrated
|
|
168
|
+
}
|
|
89
169
|
} catch (err) {
|
|
90
|
-
|
|
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
|
-
|
|
107
|
-
|
|
176
|
+
// Ensure ~/.claude/ exists
|
|
177
|
+
if (!fs.existsSync(this.claudeDir)) {
|
|
178
|
+
fs.mkdirSync(this.claudeDir, { recursive: true, mode: 0o700 });
|
|
108
179
|
}
|
|
109
180
|
|
|
110
|
-
//
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
188
|
+
try {
|
|
189
|
+
const stats = fs.lstatSync(sharedPath);
|
|
190
|
+
if (!stats.isDirectory()) continue;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
130
194
|
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
// Create shared directories
|
|
228
|
+
// Now run ensureSharedDirectories to create symlinks
|
|
150
229
|
this.ensureSharedDirectories();
|
|
151
230
|
|
|
152
|
-
//
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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
|
-
|
|
260
|
+
_copyDirectoryFallback(src, dest) {
|
|
188
261
|
if (!fs.existsSync(src)) {
|
|
189
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
package/scripts/postinstall.js
CHANGED
|
@@ -89,15 +89,16 @@ function createConfigFiles() {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// Migrate from
|
|
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.
|
|
97
|
+
sharedManager.migrateFromV311();
|
|
98
|
+
sharedManager.ensureSharedDirectories();
|
|
98
99
|
} catch (err) {
|
|
99
100
|
console.warn('[!] Migration warning:', err.message);
|
|
100
|
-
console.warn('
|
|
101
|
+
console.warn(' Migration will retry on next run');
|
|
101
102
|
}
|
|
102
103
|
console.log('');
|
|
103
104
|
|