@kaitranntt/ccs 3.1.0 → 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/ccs.js +0 -5
- package/bin/shared-manager.js +175 -54
- package/lib/ccs +1 -4
- package/lib/ccs.ps1 +1 -4
- package/package.json +1 -1
- package/scripts/postinstall.js +13 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.2.0
|
package/bin/ccs.js
CHANGED
|
@@ -262,11 +262,6 @@ async function main() {
|
|
|
262
262
|
recovery.showRecoveryHints();
|
|
263
263
|
}
|
|
264
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
|
-
|
|
270
265
|
// Detect profile
|
|
271
266
|
const { profile, remainingArgs } = detectProfile(args);
|
|
272
267
|
|
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,81 +155,116 @@ class SharedManager {
|
|
|
69
155
|
}
|
|
70
156
|
|
|
71
157
|
/**
|
|
72
|
-
* Migrate
|
|
73
|
-
*
|
|
158
|
+
* Migrate from v3.1.1 (copied data in ~/.ccs/shared/) to v3.2.0 (symlinks to ~/.claude/)
|
|
159
|
+
* Runs once on upgrade
|
|
74
160
|
*/
|
|
75
|
-
|
|
76
|
-
// Check if migration
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
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
|
|
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)) {
|
|
165
|
+
try {
|
|
166
|
+
if (fs.lstatSync(commandsPath).isSymbolicLink()) {
|
|
167
|
+
return; // Already migrated
|
|
86
168
|
}
|
|
87
|
-
})
|
|
169
|
+
} catch (err) {
|
|
170
|
+
// Continue with migration
|
|
171
|
+
}
|
|
172
|
+
}
|
|
88
173
|
|
|
89
|
-
|
|
90
|
-
|
|
174
|
+
console.log('[i] Migrating from v3.1.1 to v3.2.0...');
|
|
175
|
+
|
|
176
|
+
// Ensure ~/.claude/ exists
|
|
177
|
+
if (!fs.existsSync(this.claudeDir)) {
|
|
178
|
+
fs.mkdirSync(this.claudeDir, { recursive: true, mode: 0o700 });
|
|
91
179
|
}
|
|
92
180
|
|
|
93
|
-
//
|
|
94
|
-
this.
|
|
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);
|
|
95
185
|
|
|
96
|
-
|
|
97
|
-
const claudeDir = path.join(this.homeDir, '.claude');
|
|
186
|
+
if (!fs.existsSync(sharedPath)) continue;
|
|
98
187
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
188
|
+
try {
|
|
189
|
+
const stats = fs.lstatSync(sharedPath);
|
|
190
|
+
if (!stats.isDirectory()) continue;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
continue;
|
|
104
193
|
}
|
|
105
194
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this._copyDirectory(skillsPath, path.join(this.sharedDir, 'skills'));
|
|
195
|
+
// Create claude dir if missing
|
|
196
|
+
if (!fs.existsSync(claudePath)) {
|
|
197
|
+
fs.mkdirSync(claudePath, { recursive: true, mode: 0o700 });
|
|
110
198
|
}
|
|
111
199
|
|
|
112
|
-
// Copy
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
}
|
|
219
|
+
|
|
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}`);
|
|
116
225
|
}
|
|
117
226
|
}
|
|
118
227
|
|
|
119
|
-
//
|
|
228
|
+
// Now run ensureSharedDirectories to create symlinks
|
|
229
|
+
this.ensureSharedDirectories();
|
|
230
|
+
|
|
231
|
+
// Update all instances to use new symlinks
|
|
120
232
|
if (fs.existsSync(this.instancesDir)) {
|
|
121
|
-
|
|
233
|
+
try {
|
|
234
|
+
const instances = fs.readdirSync(this.instancesDir);
|
|
122
235
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
|
127
245
|
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
// No instances to update
|
|
128
248
|
}
|
|
129
249
|
}
|
|
130
250
|
|
|
131
|
-
console.log('[OK]
|
|
251
|
+
console.log('[OK] Migration to v3.2.0 complete');
|
|
132
252
|
}
|
|
133
253
|
|
|
134
254
|
/**
|
|
135
|
-
* Copy directory
|
|
255
|
+
* Copy directory as fallback (Windows without Developer Mode)
|
|
136
256
|
* @param {string} src - Source directory
|
|
137
257
|
* @param {string} dest - Destination directory
|
|
138
258
|
* @private
|
|
139
259
|
*/
|
|
140
|
-
|
|
260
|
+
_copyDirectoryFallback(src, dest) {
|
|
141
261
|
if (!fs.existsSync(src)) {
|
|
262
|
+
fs.mkdirSync(src, { recursive: true, mode: 0o700 });
|
|
142
263
|
return;
|
|
143
264
|
}
|
|
144
265
|
|
|
145
266
|
if (!fs.existsSync(dest)) {
|
|
146
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
267
|
+
fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
147
268
|
}
|
|
148
269
|
|
|
149
270
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
@@ -153,7 +274,7 @@ class SharedManager {
|
|
|
153
274
|
const destPath = path.join(dest, entry.name);
|
|
154
275
|
|
|
155
276
|
if (entry.isDirectory()) {
|
|
156
|
-
this.
|
|
277
|
+
this._copyDirectoryFallback(srcPath, destPath);
|
|
157
278
|
} else {
|
|
158
279
|
fs.copyFileSync(srcPath, destPath);
|
|
159
280
|
}
|
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"
|
|
@@ -1010,9 +1010,6 @@ auto_recover || {
|
|
|
1010
1010
|
exit 1
|
|
1011
1011
|
}
|
|
1012
1012
|
|
|
1013
|
-
# Run migration to shared structure (Phase 1: idempotent)
|
|
1014
|
-
migrate_to_shared_structure
|
|
1015
|
-
|
|
1016
1013
|
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
|
|
1017
1014
|
if [[ $# -eq 0 ]] || [[ "${1}" =~ ^- ]]; then
|
|
1018
1015
|
# 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.
|
|
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"
|
|
@@ -1000,9 +1000,6 @@ if (-not (Invoke-AutoRecovery)) {
|
|
|
1000
1000
|
exit 1
|
|
1001
1001
|
}
|
|
1002
1002
|
|
|
1003
|
-
# Run migration to shared structure (Phase 1: idempotent)
|
|
1004
|
-
Migrate-SharedStructure
|
|
1005
|
-
|
|
1006
1003
|
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
|
|
1007
1004
|
if ($RemainingArgs.Count -eq 0 -or $RemainingArgs[0] -match '^-') {
|
|
1008
1005
|
# No args or first arg is a flag → use default profile
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -89,6 +89,19 @@ function createConfigFiles() {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Migrate from v3.1.1 to v3.2.0 (symlink architecture)
|
|
93
|
+
console.log('');
|
|
94
|
+
try {
|
|
95
|
+
const SharedManager = require('../bin/shared-manager');
|
|
96
|
+
const sharedManager = new SharedManager();
|
|
97
|
+
sharedManager.migrateFromV311();
|
|
98
|
+
sharedManager.ensureSharedDirectories();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn('[!] Migration warning:', err.message);
|
|
101
|
+
console.warn(' Migration will retry on next run');
|
|
102
|
+
}
|
|
103
|
+
console.log('');
|
|
104
|
+
|
|
92
105
|
// Create config.json if missing
|
|
93
106
|
const configPath = path.join(ccsDir, 'config.json');
|
|
94
107
|
if (!fs.existsSync(configPath)) {
|