@kaitranntt/ccs 4.3.9 → 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 CHANGED
@@ -15,7 +15,7 @@ Stop hitting rate limits. Keep working continuously.
15
15
  [![npm](https://img.shields.io/npm/v/@kaitranntt/ccs?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@kaitranntt/ccs)
16
16
  [![PoweredBy](https://img.shields.io/badge/PoweredBy-ClaudeKit-C15F3C?style=for-the-badge)](https://claudekit.cc?ref=HMNKXOHN)
17
17
 
18
- **Languages**: [English](README.md) | [Tiếng Việt](README.vi.md) | [日本語](README.ja.md)
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 (v3.1+)
317
-
318
- **CCS items (v4.1)**: Commands and skills symlinked from `~/.ccs/.claude/` to `~/.claude/` - **single source of truth with auto-propagation**.
319
-
320
- **Profile access**: `~/.ccs/shared/` symlinks to `~/.claude/` - **no duplication across profiles**.
321
-
322
- ```plaintext
323
- ~/.ccs/
324
- ├── .claude/ # CCS items (ships with package, v4.1)
325
- │ ├── commands/ccs/ # Delegation commands (/ccs, /ccs:continue)
326
- │ └── skills/ccs-delegation/ # AI decision framework (replaces deprecated agents)
327
- ├── shared/ # Symlinks to ~/.claude/ (for profiles)
328
- │ ├── agents@ → ~/.claude/agents/
329
- │ ├── commands@ → ~/.claude/commands/
330
- │ └── skills@ ~/.claude/skills/
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.3.9
1
+ 4.4.0
package/bin/ccs.js CHANGED
@@ -533,65 +533,102 @@ async function handleUpdateCommand() {
533
533
  if (isNpmInstall) {
534
534
  // npm installation - detect package manager and update
535
535
  const packageManager = detectPackageManager();
536
- let updateCommand, updateArgs;
536
+ let updateCommand, updateArgs, cacheCommand, cacheArgs;
537
537
 
538
538
  switch (packageManager) {
539
539
  case 'npm':
540
540
  updateCommand = 'npm';
541
541
  updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
542
+ cacheCommand = 'npm';
543
+ cacheArgs = ['cache', 'clean', '--force'];
542
544
  break;
543
545
  case 'yarn':
544
546
  updateCommand = 'yarn';
545
547
  updateArgs = ['global', 'add', '@kaitranntt/ccs@latest'];
548
+ cacheCommand = 'yarn';
549
+ cacheArgs = ['cache', 'clean'];
546
550
  break;
547
551
  case 'pnpm':
548
552
  updateCommand = 'pnpm';
549
553
  updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
554
+ cacheCommand = 'pnpm';
555
+ cacheArgs = ['store', 'prune'];
550
556
  break;
551
557
  case 'bun':
552
558
  updateCommand = 'bun';
553
559
  updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
560
+ cacheCommand = null; // bun doesn't need explicit cache clearing
561
+ cacheArgs = null;
554
562
  break;
555
563
  default:
556
564
  updateCommand = 'npm';
557
565
  updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
566
+ cacheCommand = 'npm';
567
+ cacheArgs = ['cache', 'clean', '--force'];
558
568
  }
559
569
 
560
570
  console.log(colored(`Updating via ${packageManager}...`, 'cyan'));
561
571
  console.log('');
562
572
 
563
- const child = spawn(updateCommand, updateArgs, {
564
- stdio: 'inherit'
565
- // No shell needed for direct commands
566
- });
573
+ // Clear package manager cache first to ensure fresh download
574
+ if (cacheCommand) {
575
+ console.log(colored('Clearing package cache...', 'cyan'));
576
+ const cacheChild = spawn(cacheCommand, cacheArgs, {
577
+ stdio: 'inherit'
578
+ });
567
579
 
568
- child.on('exit', (code) => {
569
- if (code === 0) {
570
- console.log('');
571
- console.log(colored('[OK] Update successful!', 'green'));
572
- console.log('');
573
- console.log(`Run ${colored('ccs --version', 'yellow')} to verify`);
574
- console.log('');
575
- } else {
580
+ cacheChild.on('exit', (code) => {
581
+ if (code !== 0) {
582
+ console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow'));
583
+ }
584
+ // Continue with update regardless of cache clearing result
585
+ performUpdate();
586
+ });
587
+
588
+ cacheChild.on('error', (err) => {
589
+ console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow'));
590
+ // Continue with update regardless of cache clearing result
591
+ performUpdate();
592
+ });
593
+ } else {
594
+ // No cache clearing needed, proceed directly
595
+ performUpdate();
596
+ }
597
+
598
+ function performUpdate() {
599
+ const child = spawn(updateCommand, updateArgs, {
600
+ stdio: 'inherit'
601
+ // No shell needed for direct commands
602
+ });
603
+
604
+ child.on('exit', (code) => {
605
+ if (code === 0) {
606
+ console.log('');
607
+ console.log(colored('[OK] Update successful!', 'green'));
608
+ console.log('');
609
+ console.log(`Run ${colored('ccs --version', 'yellow')} to verify`);
610
+ console.log('');
611
+ } else {
612
+ console.log('');
613
+ console.log(colored('[X] Update failed', 'red'));
614
+ console.log('');
615
+ console.log('Try manually:');
616
+ console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
617
+ console.log('');
618
+ }
619
+ process.exit(code || 0);
620
+ });
621
+
622
+ child.on('error', (err) => {
576
623
  console.log('');
577
- console.log(colored('[X] Update failed', 'red'));
624
+ console.log(colored(`[X] Failed to run ${packageManager} update`, 'red'));
578
625
  console.log('');
579
626
  console.log('Try manually:');
580
627
  console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
581
628
  console.log('');
582
- }
583
- process.exit(code || 0);
584
- });
585
-
586
- child.on('error', (err) => {
587
- console.log('');
588
- console.log(colored(`[X] Failed to run ${packageManager} update`, 'red'));
589
- console.log('');
590
- console.log('Try manually:');
591
- console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
592
- console.log('');
593
- process.exit(1);
594
- });
629
+ process.exit(1);
630
+ });
631
+ }
595
632
  } else {
596
633
  // Direct installation - re-run installer
597
634
  console.log(colored('Updating via installer...', 'cyan'));
@@ -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
- const globalConfigDir = path.join(os.homedir(), '.claude');
163
-
164
- // Copy settings.json only (commands/skills are now symlinked to shared/)
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.sharedDirs = ['commands', 'skills', 'agents', 'plugins'];
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 dir of this.sharedDirs) {
78
- const claudePath = path.join(this.claudeDir, dir);
79
- const sharedPath = path.join(this.sharedDir, dir);
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 directory in ~/.claude/ if missing
87
+ // Create in ~/.claude/ if missing
82
88
  if (!fs.existsSync(claudePath)) {
83
- fs.mkdirSync(claudePath, { recursive: true, mode: 0o700 });
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 ${dir}: circular symlink detected`);
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
- fs.rmSync(sharedPath, { recursive: true, force: true });
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
- fs.symlinkSync(claudePath, sharedPath, 'dir');
128
+ const symlinkType = item.type === 'directory' ? 'dir' : 'file';
129
+ fs.symlinkSync(claudePath, sharedPath, symlinkType);
114
130
  } catch (err) {
115
- // Windows fallback: copy directory
131
+ // Windows fallback: copy
116
132
  if (process.platform === 'win32') {
117
- this._copyDirectoryFallback(claudePath, sharedPath);
118
- console.log(`[!] Symlink failed for ${dir}, copied instead (enable Developer Mode)`);
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 dir of this.sharedDirs) {
134
- const linkPath = path.join(instancePath, dir);
135
- const targetPath = path.join(this.sharedDir, dir);
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
- fs.rmSync(linkPath, { recursive: true, force: true });
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
- fs.symlinkSync(targetPath, linkPath, 'dir');
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
- this._copyDirectoryFallback(targetPath, linkPath);
149
- console.log(`[!] Symlink failed for ${dir}, copied instead (enable Developer Mode)`);
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 dir of this.sharedDirs) {
183
- const sharedPath = path.join(this.sharedDir, dir);
184
- const claudePath = path.join(this.claudeDir, dir);
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
- // Create claude dir if missing
196
- if (!fs.existsSync(claudePath)) {
197
- fs.mkdirSync(claudePath, { recursive: true, mode: 0o700 });
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
- // Copy files from shared to claude (preserve user modifications)
201
- try {
202
- const entries = fs.readdirSync(sharedPath, { withFileTypes: true });
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
- for (const entry of entries) {
206
- const src = path.join(sharedPath, entry.name);
207
- const dest = path.join(claudePath, entry.name);
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
- // Skip if already exists in claude
210
- if (fs.existsSync(dest)) continue;
235
+ // Skip if already exists in claude
236
+ if (fs.existsSync(dest)) continue;
211
237
 
212
- if (entry.isDirectory()) {
213
- fs.cpSync(src, dest, { recursive: true });
214
- } else {
215
- fs.copyFileSync(src, dest);
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
- if (copied > 0) {
221
- console.log(`[OK] Migrated ${copied} ${dir} to ~/.claude/${dir}`);
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 ${dir}: ${err.message}`);
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