@kaitranntt/ccs 4.3.10 → 4.4.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 +16 -38
- package/VERSION +1 -1
- package/bin/management/doctor.js +122 -0
- package/bin/management/instance-manager.js +3 -8
- package/bin/management/shared-manager.js +166 -49
- package/lib/ccs +1 -1
- package/lib/ccs.ps1 +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +3 -0
- package/README.ja.md +0 -649
- package/README.vi.md +0 -649
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Stop hitting rate limits. Keep working continuously.
|
|
|
15
15
|
[](https://www.npmjs.com/package/@kaitranntt/ccs)
|
|
16
16
|
[](https://claudekit.cc?ref=HMNKXOHN)
|
|
17
17
|
|
|
18
|
-
**Languages**: [English](README.md) | [Tiếng Việt](README.
|
|
18
|
+
**Languages**: [English](README.md) | [Tiếng Việt](docs/vi/README.md) | [日本語](docs/ja/README.md)
|
|
19
19
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
@@ -313,43 +313,21 @@ graph LR
|
|
|
313
313
|
- Uses `CLAUDE_CONFIG_DIR` for isolated instances
|
|
314
314
|
- Create with `ccs auth create <profile>`
|
|
315
315
|
|
|
316
|
-
### Shared Data (
|
|
317
|
-
|
|
318
|
-
**
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
├── instances/ # Profile-specific data
|
|
332
|
-
│ └── work/
|
|
333
|
-
│ ├── agents@ → shared/agents/
|
|
334
|
-
│ ├── commands@ → shared/commands/
|
|
335
|
-
│ ├── skills@ → shared/skills/
|
|
336
|
-
│ ├── settings.json # API keys, credentials
|
|
337
|
-
│ ├── sessions/ # Conversation history
|
|
338
|
-
│ └── ...
|
|
339
|
-
|
|
340
|
-
~/.claude/ # User's Claude directory
|
|
341
|
-
├── commands/ccs@ → ~/.ccs/.claude/commands/ccs/ # Selective symlink
|
|
342
|
-
├── skills/ccs-delegation@ → ~/.ccs/.claude/skills/ccs-delegation/
|
|
343
|
-
# agents/ccs-delegator.md@ → ~/.ccs/.claude/agents/ccs-delegator.md # Deprecated in v4.3.2
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
**Symlink Chain**: `work profile → ~/.ccs/shared/ → ~/.claude/ → ~/.ccs/.claude/` (CCS items)
|
|
347
|
-
|
|
348
|
-
| Type | Files |
|
|
349
|
-
|:-----|:------|
|
|
350
|
-
| **CCS items** | `~/.ccs/.claude/` (ships with package, selective symlinks to `~/.claude/`) |
|
|
351
|
-
| **Shared** | `~/.ccs/shared/` (symlinks to `~/.claude/`) |
|
|
352
|
-
| **Profile-specific** | `settings.json`, `sessions/`, `todolists/`, `logs/` |
|
|
316
|
+
### Shared Data (v4.4+)
|
|
317
|
+
|
|
318
|
+
**Shared across instances** (`~/.ccs/shared/` symlinked to `~/.claude/`):
|
|
319
|
+
- commands/ - Slash commands
|
|
320
|
+
- skills/ - Agent skills
|
|
321
|
+
- agents/ - Agent configs
|
|
322
|
+
- **settings.json** - Claude CLI settings (v4.4+)
|
|
323
|
+
|
|
324
|
+
**Profile-specific**:
|
|
325
|
+
- sessions/ - Conversation history
|
|
326
|
+
- todolists/ - Todo lists
|
|
327
|
+
- logs/ - Execution logs
|
|
328
|
+
|
|
329
|
+
> [!NOTE]
|
|
330
|
+
> **v4.4 Breaking Change**: settings.json now shared across profiles. Previously each profile had isolated settings. Migration is automatic on install using ~/.claude/settings.json as the authoritative source. Backups created: `<instance>/settings.json.pre-shared-migration`
|
|
353
331
|
|
|
354
332
|
> [!NOTE]
|
|
355
333
|
> **Windows**: Symlink support requires Developer Mode (v4.2 will add copy fallback)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.4.0
|
package/bin/management/doctor.js
CHANGED
|
@@ -117,6 +117,7 @@ class Doctor {
|
|
|
117
117
|
console.log(colored('System Health:', 'bold'));
|
|
118
118
|
this.checkPermissions();
|
|
119
119
|
this.checkCcsSymlinks();
|
|
120
|
+
this.checkSettingsSymlinks();
|
|
120
121
|
console.log('');
|
|
121
122
|
|
|
122
123
|
this.showReport();
|
|
@@ -502,6 +503,127 @@ class Doctor {
|
|
|
502
503
|
}
|
|
503
504
|
}
|
|
504
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Check 10: settings.json symlinks
|
|
508
|
+
*/
|
|
509
|
+
checkSettingsSymlinks() {
|
|
510
|
+
const spinner = ora('Checking settings.json symlinks').start();
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const sharedDir = path.join(this.homedir, '.ccs', 'shared');
|
|
514
|
+
const sharedSettings = path.join(sharedDir, 'settings.json');
|
|
515
|
+
const claudeSettings = path.join(this.claudeDir, 'settings.json');
|
|
516
|
+
|
|
517
|
+
// Check shared settings exists and points to ~/.claude/
|
|
518
|
+
if (!fs.existsSync(sharedSettings)) {
|
|
519
|
+
spinner.warn(` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Not found`);
|
|
520
|
+
this.results.addCheck(
|
|
521
|
+
'Settings Symlinks',
|
|
522
|
+
'warning',
|
|
523
|
+
'Shared settings.json not found',
|
|
524
|
+
'Run: ccs sync'
|
|
525
|
+
);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const sharedStats = fs.lstatSync(sharedSettings);
|
|
530
|
+
if (!sharedStats.isSymbolicLink()) {
|
|
531
|
+
spinner.warn(` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Not a symlink`);
|
|
532
|
+
this.results.addCheck(
|
|
533
|
+
'Settings Symlinks',
|
|
534
|
+
'warning',
|
|
535
|
+
'Shared settings.json is not a symlink',
|
|
536
|
+
'Run: ccs sync'
|
|
537
|
+
);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const sharedTarget = fs.readlinkSync(sharedSettings);
|
|
542
|
+
const resolvedShared = path.resolve(path.dirname(sharedSettings), sharedTarget);
|
|
543
|
+
|
|
544
|
+
if (resolvedShared !== claudeSettings) {
|
|
545
|
+
spinner.warn(` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Wrong target`);
|
|
546
|
+
this.results.addCheck(
|
|
547
|
+
'Settings Symlinks',
|
|
548
|
+
'warning',
|
|
549
|
+
`Points to ${resolvedShared} instead of ${claudeSettings}`,
|
|
550
|
+
'Run: ccs sync'
|
|
551
|
+
);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Check each instance
|
|
556
|
+
const instancesDir = path.join(this.ccsDir, 'instances');
|
|
557
|
+
if (!fs.existsSync(instancesDir)) {
|
|
558
|
+
spinner.succeed(` ${'settings.json'.padEnd(26)}${colored('[OK]', 'green')} Shared symlink valid`);
|
|
559
|
+
this.results.addCheck('Settings Symlinks', 'success', 'Shared symlink valid', null, {
|
|
560
|
+
status: 'OK',
|
|
561
|
+
info: 'Shared symlink valid'
|
|
562
|
+
});
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const instances = fs.readdirSync(instancesDir).filter(name => {
|
|
567
|
+
return fs.statSync(path.join(instancesDir, name)).isDirectory();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
let broken = 0;
|
|
571
|
+
for (const instance of instances) {
|
|
572
|
+
const instancePath = path.join(instancesDir, instance);
|
|
573
|
+
const instanceSettings = path.join(instancePath, 'settings.json');
|
|
574
|
+
|
|
575
|
+
if (!fs.existsSync(instanceSettings)) {
|
|
576
|
+
broken++;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const stats = fs.lstatSync(instanceSettings);
|
|
582
|
+
if (!stats.isSymbolicLink()) {
|
|
583
|
+
broken++;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const target = fs.readlinkSync(instanceSettings);
|
|
588
|
+
const resolved = path.resolve(path.dirname(instanceSettings), target);
|
|
589
|
+
|
|
590
|
+
if (resolved !== sharedSettings) {
|
|
591
|
+
broken++;
|
|
592
|
+
}
|
|
593
|
+
} catch (err) {
|
|
594
|
+
broken++;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (broken > 0) {
|
|
599
|
+
spinner.warn(` ${'settings.json'.padEnd(26)}${colored('[!]', 'yellow')} ${broken} broken instance(s)`);
|
|
600
|
+
this.results.addCheck(
|
|
601
|
+
'Settings Symlinks',
|
|
602
|
+
'warning',
|
|
603
|
+
`${broken} instance(s) have broken symlinks`,
|
|
604
|
+
'Run: ccs sync',
|
|
605
|
+
{ status: 'WARN', info: `${broken} broken instance(s)` }
|
|
606
|
+
);
|
|
607
|
+
} else {
|
|
608
|
+
spinner.succeed(` ${'settings.json'.padEnd(26)}${colored('[OK]', 'green')} ${instances.length} instance(s) valid`);
|
|
609
|
+
this.results.addCheck('Settings Symlinks', 'success', 'All instance symlinks valid', null, {
|
|
610
|
+
status: 'OK',
|
|
611
|
+
info: `${instances.length} instance(s) valid`
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
} catch (err) {
|
|
616
|
+
spinner.warn(` ${'settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Check failed`);
|
|
617
|
+
this.results.addCheck(
|
|
618
|
+
'Settings Symlinks',
|
|
619
|
+
'warning',
|
|
620
|
+
`Failed to check: ${err.message}`,
|
|
621
|
+
'Run: ccs sync',
|
|
622
|
+
{ status: 'WARN', info: 'Check failed' }
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
505
627
|
/**
|
|
506
628
|
* Show health check report
|
|
507
629
|
*/
|
|
@@ -159,14 +159,9 @@ class InstanceManager {
|
|
|
159
159
|
* @param {string} instancePath - Instance path
|
|
160
160
|
*/
|
|
161
161
|
_copyGlobalConfigs(instancePath) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
const globalSettings = path.join(globalConfigDir, 'settings.json');
|
|
166
|
-
if (fs.existsSync(globalSettings)) {
|
|
167
|
-
const instanceSettings = path.join(instancePath, 'settings.json');
|
|
168
|
-
fs.copyFileSync(globalSettings, instanceSettings);
|
|
169
|
-
}
|
|
162
|
+
// No longer needed - settings.json now symlinked via SharedManager
|
|
163
|
+
// Keeping method for backward compatibility (empty implementation)
|
|
164
|
+
// Can be removed in future major version
|
|
170
165
|
}
|
|
171
166
|
|
|
172
167
|
/**
|
|
@@ -17,7 +17,13 @@ class SharedManager {
|
|
|
17
17
|
this.sharedDir = path.join(this.homeDir, '.ccs', 'shared');
|
|
18
18
|
this.claudeDir = path.join(this.homeDir, '.claude');
|
|
19
19
|
this.instancesDir = path.join(this.homeDir, '.ccs', 'instances');
|
|
20
|
-
this.
|
|
20
|
+
this.sharedItems = [
|
|
21
|
+
{ name: 'commands', type: 'directory' },
|
|
22
|
+
{ name: 'skills', type: 'directory' },
|
|
23
|
+
{ name: 'agents', type: 'directory' },
|
|
24
|
+
{ name: 'plugins', type: 'directory' },
|
|
25
|
+
{ name: 'settings.json', type: 'file' }
|
|
26
|
+
];
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/**
|
|
@@ -74,18 +80,23 @@ class SharedManager {
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
// Create symlinks ~/.ccs/shared/* → ~/.claude/*
|
|
77
|
-
for (const
|
|
78
|
-
const claudePath = path.join(this.claudeDir,
|
|
79
|
-
const sharedPath = path.join(this.sharedDir,
|
|
83
|
+
for (const item of this.sharedItems) {
|
|
84
|
+
const claudePath = path.join(this.claudeDir, item.name);
|
|
85
|
+
const sharedPath = path.join(this.sharedDir, item.name);
|
|
80
86
|
|
|
81
|
-
// Create
|
|
87
|
+
// Create in ~/.claude/ if missing
|
|
82
88
|
if (!fs.existsSync(claudePath)) {
|
|
83
|
-
|
|
89
|
+
if (item.type === 'directory') {
|
|
90
|
+
fs.mkdirSync(claudePath, { recursive: true, mode: 0o700 });
|
|
91
|
+
} else if (item.type === 'file') {
|
|
92
|
+
// Create empty settings.json if missing
|
|
93
|
+
fs.writeFileSync(claudePath, JSON.stringify({}, null, 2), 'utf8');
|
|
94
|
+
}
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
// Check for circular symlink
|
|
87
98
|
if (this._detectCircularSymlink(claudePath, sharedPath)) {
|
|
88
|
-
console.log(`[!] Skipping ${
|
|
99
|
+
console.log(`[!] Skipping ${item.name}: circular symlink detected`);
|
|
89
100
|
continue;
|
|
90
101
|
}
|
|
91
102
|
|
|
@@ -104,18 +115,27 @@ class SharedManager {
|
|
|
104
115
|
// Continue to recreate
|
|
105
116
|
}
|
|
106
117
|
|
|
107
|
-
// Remove existing directory/link
|
|
108
|
-
|
|
118
|
+
// Remove existing file/directory/link
|
|
119
|
+
if (item.type === 'directory') {
|
|
120
|
+
fs.rmSync(sharedPath, { recursive: true, force: true });
|
|
121
|
+
} else {
|
|
122
|
+
fs.unlinkSync(sharedPath);
|
|
123
|
+
}
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
// Create symlink
|
|
112
127
|
try {
|
|
113
|
-
|
|
128
|
+
const symlinkType = item.type === 'directory' ? 'dir' : 'file';
|
|
129
|
+
fs.symlinkSync(claudePath, sharedPath, symlinkType);
|
|
114
130
|
} catch (err) {
|
|
115
|
-
// Windows fallback: copy
|
|
131
|
+
// Windows fallback: copy
|
|
116
132
|
if (process.platform === 'win32') {
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
if (item.type === 'directory') {
|
|
134
|
+
this._copyDirectoryFallback(claudePath, sharedPath);
|
|
135
|
+
} else if (item.type === 'file') {
|
|
136
|
+
fs.copyFileSync(claudePath, sharedPath);
|
|
137
|
+
}
|
|
138
|
+
console.log(`[!] Symlink failed for ${item.name}, copied instead (enable Developer Mode)`);
|
|
119
139
|
} else {
|
|
120
140
|
throw err;
|
|
121
141
|
}
|
|
@@ -130,23 +150,32 @@ class SharedManager {
|
|
|
130
150
|
linkSharedDirectories(instancePath) {
|
|
131
151
|
this.ensureSharedDirectories();
|
|
132
152
|
|
|
133
|
-
for (const
|
|
134
|
-
const linkPath = path.join(instancePath,
|
|
135
|
-
const targetPath = path.join(this.sharedDir,
|
|
153
|
+
for (const item of this.sharedItems) {
|
|
154
|
+
const linkPath = path.join(instancePath, item.name);
|
|
155
|
+
const targetPath = path.join(this.sharedDir, item.name);
|
|
136
156
|
|
|
137
|
-
// Remove existing directory/link
|
|
157
|
+
// Remove existing file/directory/link
|
|
138
158
|
if (fs.existsSync(linkPath)) {
|
|
139
|
-
|
|
159
|
+
if (item.type === 'directory') {
|
|
160
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
161
|
+
} else {
|
|
162
|
+
fs.unlinkSync(linkPath);
|
|
163
|
+
}
|
|
140
164
|
}
|
|
141
165
|
|
|
142
166
|
// Create symlink
|
|
143
167
|
try {
|
|
144
|
-
|
|
168
|
+
const symlinkType = item.type === 'directory' ? 'dir' : 'file';
|
|
169
|
+
fs.symlinkSync(targetPath, linkPath, symlinkType);
|
|
145
170
|
} catch (err) {
|
|
146
171
|
// Windows fallback
|
|
147
172
|
if (process.platform === 'win32') {
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
if (item.type === 'directory') {
|
|
174
|
+
this._copyDirectoryFallback(targetPath, linkPath);
|
|
175
|
+
} else if (item.type === 'file') {
|
|
176
|
+
fs.copyFileSync(targetPath, linkPath);
|
|
177
|
+
}
|
|
178
|
+
console.log(`[!] Symlink failed for ${item.name}, copied instead (enable Developer Mode)`);
|
|
150
179
|
} else {
|
|
151
180
|
throw err;
|
|
152
181
|
}
|
|
@@ -179,49 +208,56 @@ class SharedManager {
|
|
|
179
208
|
}
|
|
180
209
|
|
|
181
210
|
// Copy user modifications from ~/.ccs/shared/ to ~/.claude/
|
|
182
|
-
for (const
|
|
183
|
-
const sharedPath = path.join(this.sharedDir,
|
|
184
|
-
const claudePath = path.join(this.claudeDir,
|
|
211
|
+
for (const item of this.sharedItems) {
|
|
212
|
+
const sharedPath = path.join(this.sharedDir, item.name);
|
|
213
|
+
const claudePath = path.join(this.claudeDir, item.name);
|
|
185
214
|
|
|
186
215
|
if (!fs.existsSync(sharedPath)) continue;
|
|
187
216
|
|
|
188
217
|
try {
|
|
189
218
|
const stats = fs.lstatSync(sharedPath);
|
|
190
|
-
if (!stats.isDirectory()) continue;
|
|
191
|
-
} catch (err) {
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
// Handle directories
|
|
221
|
+
if (item.type === 'directory' && stats.isDirectory()) {
|
|
222
|
+
// Create claude dir if missing
|
|
223
|
+
if (!fs.existsSync(claudePath)) {
|
|
224
|
+
fs.mkdirSync(claudePath, { recursive: true, mode: 0o700 });
|
|
225
|
+
}
|
|
199
226
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
let copied = 0;
|
|
227
|
+
// Copy files from shared to claude (preserve user modifications)
|
|
228
|
+
const entries = fs.readdirSync(sharedPath, { withFileTypes: true });
|
|
229
|
+
let copied = 0;
|
|
204
230
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
const src = path.join(sharedPath, entry.name);
|
|
233
|
+
const dest = path.join(claudePath, entry.name);
|
|
208
234
|
|
|
209
|
-
|
|
210
|
-
|
|
235
|
+
// Skip if already exists in claude
|
|
236
|
+
if (fs.existsSync(dest)) continue;
|
|
211
237
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
238
|
+
if (entry.isDirectory()) {
|
|
239
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
240
|
+
} else {
|
|
241
|
+
fs.copyFileSync(src, dest);
|
|
242
|
+
}
|
|
243
|
+
copied++;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (copied > 0) {
|
|
247
|
+
console.log(`[OK] Migrated ${copied} ${item.name} to ~/.claude/${item.name}`);
|
|
216
248
|
}
|
|
217
|
-
copied++;
|
|
218
249
|
}
|
|
219
250
|
|
|
220
|
-
|
|
221
|
-
|
|
251
|
+
// Handle files (settings.json)
|
|
252
|
+
else if (item.type === 'file' && stats.isFile()) {
|
|
253
|
+
// Only copy if ~/.claude/ version doesn't exist
|
|
254
|
+
if (!fs.existsSync(claudePath)) {
|
|
255
|
+
fs.copyFileSync(sharedPath, claudePath);
|
|
256
|
+
console.log(`[OK] Migrated ${item.name} to ~/.claude/${item.name}`);
|
|
257
|
+
}
|
|
222
258
|
}
|
|
223
259
|
} catch (err) {
|
|
224
|
-
console.log(`[!] Failed to migrate ${
|
|
260
|
+
console.log(`[!] Failed to migrate ${item.name}: ${err.message}`);
|
|
225
261
|
}
|
|
226
262
|
}
|
|
227
263
|
|
|
@@ -251,6 +287,87 @@ class SharedManager {
|
|
|
251
287
|
console.log('[OK] Migration to v3.2.0 complete');
|
|
252
288
|
}
|
|
253
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Migrate existing instances from isolated to shared settings.json (v4.4+)
|
|
292
|
+
* Runs once on upgrade
|
|
293
|
+
*/
|
|
294
|
+
migrateToSharedSettings() {
|
|
295
|
+
console.log('[i] Migrating instances to shared settings.json...');
|
|
296
|
+
|
|
297
|
+
// Ensure ~/.claude/settings.json exists (authoritative source)
|
|
298
|
+
const claudeSettings = path.join(this.claudeDir, 'settings.json');
|
|
299
|
+
if (!fs.existsSync(claudeSettings)) {
|
|
300
|
+
// Create empty settings if missing
|
|
301
|
+
fs.writeFileSync(claudeSettings, JSON.stringify({}, null, 2), 'utf8');
|
|
302
|
+
console.log('[i] Created ~/.claude/settings.json');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Ensure shared settings.json symlink exists
|
|
306
|
+
this.ensureSharedDirectories();
|
|
307
|
+
|
|
308
|
+
// Migrate each instance
|
|
309
|
+
if (!fs.existsSync(this.instancesDir)) {
|
|
310
|
+
console.log('[i] No instances to migrate');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const instances = fs.readdirSync(this.instancesDir).filter(name => {
|
|
315
|
+
const instancePath = path.join(this.instancesDir, name);
|
|
316
|
+
return fs.statSync(instancePath).isDirectory();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
let migrated = 0;
|
|
320
|
+
let skipped = 0;
|
|
321
|
+
|
|
322
|
+
for (const instance of instances) {
|
|
323
|
+
const instancePath = path.join(this.instancesDir, instance);
|
|
324
|
+
const instanceSettings = path.join(instancePath, 'settings.json');
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
// Check if already symlink
|
|
328
|
+
if (fs.existsSync(instanceSettings)) {
|
|
329
|
+
const stats = fs.lstatSync(instanceSettings);
|
|
330
|
+
if (stats.isSymbolicLink()) {
|
|
331
|
+
skipped++;
|
|
332
|
+
continue; // Already migrated
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Backup existing settings
|
|
336
|
+
const backup = instanceSettings + '.pre-shared-migration';
|
|
337
|
+
if (!fs.existsSync(backup)) {
|
|
338
|
+
fs.copyFileSync(instanceSettings, backup);
|
|
339
|
+
console.log(`[i] Backed up ${instance}/settings.json`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Remove old settings.json
|
|
343
|
+
fs.unlinkSync(instanceSettings);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Create symlink via SharedManager
|
|
347
|
+
const sharedSettings = path.join(this.sharedDir, 'settings.json');
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
fs.symlinkSync(sharedSettings, instanceSettings, 'file');
|
|
351
|
+
migrated++;
|
|
352
|
+
} catch (err) {
|
|
353
|
+
// Windows fallback
|
|
354
|
+
if (process.platform === 'win32') {
|
|
355
|
+
fs.copyFileSync(sharedSettings, instanceSettings);
|
|
356
|
+
console.log(`[!] Symlink failed for ${instance}, copied instead`);
|
|
357
|
+
migrated++;
|
|
358
|
+
} else {
|
|
359
|
+
throw err;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.log(`[!] Failed to migrate ${instance}: ${err.message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
console.log(`[OK] Migrated ${migrated} instance(s), skipped ${skipped}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
254
371
|
/**
|
|
255
372
|
* Copy directory as fallback (Windows without Developer Mode)
|
|
256
373
|
* @param {string} src - Source directory
|
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="4.
|
|
5
|
+
CCS_VERSION="4.4.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 = "4.
|
|
15
|
+
$CcsVersion = "4.4.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
|
@@ -97,6 +97,9 @@ function createConfigFiles() {
|
|
|
97
97
|
const sharedManager = new SharedManager();
|
|
98
98
|
sharedManager.migrateFromV311();
|
|
99
99
|
sharedManager.ensureSharedDirectories();
|
|
100
|
+
|
|
101
|
+
// Run v4.4 migration: Migrate instances to shared settings.json
|
|
102
|
+
sharedManager.migrateToSharedSettings();
|
|
100
103
|
} catch (err) {
|
|
101
104
|
console.warn('[!] Migration warning:', err.message);
|
|
102
105
|
console.warn(' Migration will retry on next run');
|