@kaitranntt/ccs 4.3.3 → 4.3.5

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
@@ -323,8 +323,7 @@ graph LR
323
323
  ~/.ccs/
324
324
  ├── .claude/ # CCS items (ships with package, v4.1)
325
325
  │ ├── commands/ccs/ # Delegation commands (/ccs, /ccs:continue)
326
- ├── skills/ccs-delegation/ # AI decision framework
327
- │ └── agents/ccs-delegator.md # Proactive delegation agent
326
+ └── skills/ccs-delegation/ # AI decision framework (replaces deprecated agents)
328
327
  ├── shared/ # Symlinks to ~/.claude/ (for profiles)
329
328
  │ ├── agents@ → ~/.claude/agents/
330
329
  │ ├── commands@ → ~/.claude/commands/
@@ -341,7 +340,7 @@ graph LR
341
340
  ~/.claude/ # User's Claude directory
342
341
  ├── commands/ccs@ → ~/.ccs/.claude/commands/ccs/ # Selective symlink
343
342
  ├── skills/ccs-delegation@ → ~/.ccs/.claude/skills/ccs-delegation/
344
- └── agents/ccs-delegator.md@ → ~/.ccs/.claude/agents/ccs-delegator.md
343
+ # agents/ccs-delegator.md@ → ~/.ccs/.claude/agents/ccs-delegator.md # Deprecated in v4.3.2
345
344
  ```
346
345
 
347
346
  **Symlink Chain**: `work profile → ~/.ccs/shared/ → ~/.claude/ → ~/.ccs/.claude/` (CCS items)
@@ -507,8 +506,8 @@ Savings: $0.0275 (86% reduction)
507
506
  ### Documentation
508
507
 
509
508
  - **Workflow Diagrams**: See [docs/ccs-delegation-diagrams.md](docs/ccs-delegation-diagrams.md) for visual architecture
510
- - **Skill Reference**: `.claude/skills/ccs-delegation/` for AI decision framework
511
- - **Agent Docs**: `.claude/agents/ccs-delegator.md` for orchestration patterns
509
+ - **Skill Reference**: `.claude/skills/ccs-delegation/` for AI decision framework (replaces deprecated agents)
510
+ - **Agent Docs**: `.claude/agents/ccs-delegator.md` was deprecated in v4.3.2, functionality moved to ccs-delegation skill
512
511
 
513
512
  <br>
514
513
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.3.3
1
+ 4.3.5
package/bin/ccs.js CHANGED
@@ -286,6 +286,11 @@ async function handleSyncCommand() {
286
286
 
287
287
  console.log('');
288
288
 
289
+ const cleanupResult = installer.cleanupDeprecated();
290
+ if (cleanupResult.success && cleanupResult.cleanedFiles.length > 0) {
291
+ console.log('');
292
+ }
293
+
289
294
  // Then, create symlinks from ~/.ccs/.claude/ to ~/.claude/
290
295
  const ClaudeSymlinkManager = require('./utils/claude-symlink-manager');
291
296
  const manager = new ClaudeSymlinkManager();
@@ -298,6 +303,150 @@ async function handleSyncCommand() {
298
303
  process.exit(0);
299
304
  }
300
305
 
306
+ /**
307
+ * Detect installation method
308
+ * @returns {'npm'|'direct'} - Installation method
309
+ */
310
+ function detectInstallationMethod() {
311
+ const scriptPath = process.argv[1];
312
+
313
+ // Method 1: Check if script is inside node_modules
314
+ if (scriptPath.includes('node_modules')) {
315
+ return 'npm';
316
+ }
317
+
318
+ // Method 2: Check if script is in npm global bin directory
319
+ // Common patterns for npm global installations
320
+ const npmGlobalBinPatterns = [
321
+ /\.npm\/global\/bin\//, // ~/.npm/global/bin/ccs
322
+ /\/\.nvm\/versions\/node\/[^\/]+\/bin\//, // ~/.nvm/versions/node/v22.19.0/bin/ccs
323
+ /\/usr\/local\/bin\//, // /usr/local/bin/ccs (if npm global prefix is /usr/local)
324
+ /\/usr\/bin\// // /usr/bin/ccs (if npm global prefix is /usr)
325
+ ];
326
+
327
+ for (const pattern of npmGlobalBinPatterns) {
328
+ if (pattern.test(scriptPath)) {
329
+ // Verify this is actually CCS by checking the linked target
330
+ try {
331
+ const binDir = path.dirname(scriptPath);
332
+ const nodeModulesDir = path.join(binDir, '..', 'lib', 'node_modules', '@kaitranntt', 'ccs');
333
+ const globalModulesDir = path.join(binDir, '..', 'node_modules', '@kaitranntt', 'ccs');
334
+
335
+ if (fs.existsSync(nodeModulesDir) || fs.existsSync(globalModulesDir)) {
336
+ return 'npm';
337
+ }
338
+ } catch (err) {
339
+ // Continue checking other patterns
340
+ }
341
+ }
342
+ }
343
+
344
+ // Method 3: Check if package.json exists in parent directory (development mode)
345
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
346
+
347
+ if (fs.existsSync(packageJsonPath)) {
348
+ try {
349
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
350
+ // If package.json has name "@kaitranntt/ccs", it's npm install
351
+ if (pkg.name === '@kaitranntt/ccs') {
352
+ return 'npm';
353
+ }
354
+ } catch (err) {
355
+ // Ignore parse errors
356
+ }
357
+ }
358
+
359
+ // Method 4: Check if script is a symlink pointing to node_modules
360
+ try {
361
+ const stats = fs.lstatSync(scriptPath);
362
+ if (stats.isSymbolicLink()) {
363
+ const targetPath = fs.readlinkSync(scriptPath);
364
+ if (targetPath.includes('node_modules') || targetPath.includes('@kaitranntt/ccs')) {
365
+ return 'npm';
366
+ }
367
+ }
368
+ } catch (err) {
369
+ // Continue to default
370
+ }
371
+
372
+ // Default to direct installation
373
+ return 'direct';
374
+ }
375
+
376
+ /**
377
+ * Detect which package manager was used for installation
378
+ * @returns {'npm'|'yarn'|'pnpm'|'bun'|'unknown'}
379
+ */
380
+ function detectPackageManager() {
381
+ const scriptPath = process.argv[1];
382
+
383
+ // Check if script path contains package manager indicators
384
+ if (scriptPath.includes('.pnpm')) return 'pnpm';
385
+ if (scriptPath.includes('yarn')) return 'yarn';
386
+ if (scriptPath.includes('bun')) return 'bun';
387
+
388
+ // Check parent directories for lock files
389
+ const binDir = path.dirname(scriptPath);
390
+ const fs = require('fs');
391
+
392
+ // Check global node_modules parent for lock files
393
+ let checkDir = binDir;
394
+ for (let i = 0; i < 5; i++) {
395
+ if (fs.existsSync(path.join(checkDir, 'pnpm-lock.yaml'))) return 'pnpm';
396
+ if (fs.existsSync(path.join(checkDir, 'yarn.lock'))) return 'yarn';
397
+ if (fs.existsSync(path.join(checkDir, 'bun.lockb'))) return 'bun';
398
+ checkDir = path.dirname(checkDir);
399
+ }
400
+
401
+ // Check if package managers are available on the system
402
+ const { spawnSync } = require('child_process');
403
+
404
+ // Try yarn global list to see if CCS is installed via yarn
405
+ try {
406
+ const yarnResult = spawnSync('yarn', ['global', 'list', '--pattern', '@kaitranntt/ccs'], {
407
+ encoding: 'utf8',
408
+ shell: true,
409
+ timeout: 5000
410
+ });
411
+ if (yarnResult.status === 0 && yarnResult.stdout.includes('@kaitranntt/ccs')) {
412
+ return 'yarn';
413
+ }
414
+ } catch (err) {
415
+ // Continue to next check
416
+ }
417
+
418
+ // Try pnpm list -g to see if CCS is installed via pnpm
419
+ try {
420
+ const pnpmResult = spawnSync('pnpm', ['list', '-g', '--pattern', '@kaitranntt/ccs'], {
421
+ encoding: 'utf8',
422
+ shell: true,
423
+ timeout: 5000
424
+ });
425
+ if (pnpmResult.status === 0 && pnpmResult.stdout.includes('@kaitranntt/ccs')) {
426
+ return 'pnpm';
427
+ }
428
+ } catch (err) {
429
+ // Continue to next check
430
+ }
431
+
432
+ // Try bun pm ls -g to see if CCS is installed via bun
433
+ try {
434
+ const bunResult = spawnSync('bun', ['pm', 'ls', '-g', '--pattern', '@kaitranntt/ccs'], {
435
+ encoding: 'utf8',
436
+ shell: true,
437
+ timeout: 5000
438
+ });
439
+ if (bunResult.status === 0 && bunResult.stdout.includes('@kaitranntt/ccs')) {
440
+ return 'bun';
441
+ }
442
+ } catch (err) {
443
+ // Continue to default
444
+ }
445
+
446
+ // Default to npm
447
+ return 'npm';
448
+ }
449
+
301
450
  async function handleUpdateCommand() {
302
451
  const { checkForUpdates } = require('./utils/update-checker');
303
452
  const { spawn } = require('child_process');
@@ -307,8 +456,8 @@ async function handleUpdateCommand() {
307
456
  console.log('');
308
457
 
309
458
  // Detect installation method for proper update source
310
- const isNpmInstall = process.argv[1].includes('node_modules');
311
- const installMethod = isNpmInstall ? 'npm' : 'direct';
459
+ const installMethod = detectInstallationMethod();
460
+ const isNpmInstall = installMethod === 'npm';
312
461
 
313
462
  // Check for updates (force check)
314
463
  const updateResult = await checkForUpdates(CCS_VERSION, true, installMethod);
@@ -323,7 +472,27 @@ async function handleUpdateCommand() {
323
472
  console.log('');
324
473
  console.log('Try again later or update manually:');
325
474
  if (isNpmInstall) {
326
- console.log(colored(' npm install -g @kaitranntt/ccs@latest', 'yellow'));
475
+ const packageManager = detectPackageManager();
476
+ let manualCommand;
477
+
478
+ switch (packageManager) {
479
+ case 'npm':
480
+ manualCommand = 'npm install -g @kaitranntt/ccs@latest';
481
+ break;
482
+ case 'yarn':
483
+ manualCommand = 'yarn global add @kaitranntt/ccs@latest';
484
+ break;
485
+ case 'pnpm':
486
+ manualCommand = 'pnpm add -g @kaitranntt/ccs@latest';
487
+ break;
488
+ case 'bun':
489
+ manualCommand = 'bun add -g @kaitranntt/ccs@latest';
490
+ break;
491
+ default:
492
+ manualCommand = 'npm install -g @kaitranntt/ccs@latest';
493
+ }
494
+
495
+ console.log(colored(` ${manualCommand}`, 'yellow'));
327
496
  } else {
328
497
  const isWindows = process.platform === 'win32';
329
498
  if (isWindows) {
@@ -361,13 +530,38 @@ async function handleUpdateCommand() {
361
530
  console.log('');
362
531
 
363
532
  if (isNpmInstall) {
364
- // npm installation - use npm update
365
- console.log(colored('Updating via npm...', 'cyan'));
533
+ // npm installation - detect package manager and update
534
+ const packageManager = detectPackageManager();
535
+ let updateCommand, updateArgs;
536
+
537
+ switch (packageManager) {
538
+ case 'npm':
539
+ updateCommand = 'npm';
540
+ updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
541
+ break;
542
+ case 'yarn':
543
+ updateCommand = 'yarn';
544
+ updateArgs = ['global', 'add', '@kaitranntt/ccs@latest'];
545
+ break;
546
+ case 'pnpm':
547
+ updateCommand = 'pnpm';
548
+ updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
549
+ break;
550
+ case 'bun':
551
+ updateCommand = 'bun';
552
+ updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
553
+ break;
554
+ default:
555
+ updateCommand = 'npm';
556
+ updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
557
+ }
558
+
559
+ console.log(colored(`Updating via ${packageManager}...`, 'cyan'));
366
560
  console.log('');
367
561
 
368
- const child = spawn('npm', ['install', '-g', '@kaitranntt/ccs@latest'], {
369
- stdio: 'inherit',
370
- shell: true
562
+ const child = spawn(updateCommand, updateArgs, {
563
+ stdio: 'inherit'
564
+ // No shell needed for direct commands
371
565
  });
372
566
 
373
567
  child.on('exit', (code) => {
@@ -382,7 +576,7 @@ async function handleUpdateCommand() {
382
576
  console.log(colored('[X] Update failed', 'red'));
383
577
  console.log('');
384
578
  console.log('Try manually:');
385
- console.log(colored(' npm install -g @kaitranntt/ccs@latest', 'yellow'));
579
+ console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
386
580
  console.log('');
387
581
  }
388
582
  process.exit(code || 0);
@@ -390,10 +584,10 @@ async function handleUpdateCommand() {
390
584
 
391
585
  child.on('error', (err) => {
392
586
  console.log('');
393
- console.log(colored('[X] Failed to run npm update', 'red'));
587
+ console.log(colored(`[X] Failed to run ${packageManager} update`, 'red'));
394
588
  console.log('');
395
589
  console.log('Try manually:');
396
- console.log(colored(' npm install -g @kaitranntt/ccs@latest', 'yellow'));
590
+ console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
397
591
  console.log('');
398
592
  process.exit(1);
399
593
  });
@@ -406,16 +600,19 @@ async function handleUpdateCommand() {
406
600
  let command, args;
407
601
 
408
602
  if (isWindows) {
603
+ // PowerShell
409
604
  command = 'powershell.exe';
410
- args = ['-Command', 'irm ccs.kaitran.ca/install | iex'];
605
+ args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
606
+ 'irm ccs.kaitran.ca/install | iex'];
411
607
  } else {
412
- command = 'bash';
608
+ // Unix (bash with proper shell invocation)
609
+ command = '/bin/bash';
413
610
  args = ['-c', 'curl -fsSL ccs.kaitran.ca/install | bash'];
414
611
  }
415
612
 
416
613
  const child = spawn(command, args, {
417
- stdio: 'inherit',
418
- shell: true
614
+ stdio: 'inherit'
615
+ // Do NOT use shell: true
419
616
  });
420
617
 
421
618
  child.on('exit', (code) => {
@@ -148,6 +148,108 @@ class ClaudeDirInstaller {
148
148
  return { files, dirs };
149
149
  }
150
150
 
151
+ /**
152
+ * Clean up deprecated files from previous installations
153
+ * Removes ccs-delegator.md that was deprecated in v4.3.2
154
+ * @param {boolean} silent - Suppress console output
155
+ */
156
+ cleanupDeprecated(silent = false) {
157
+ const deprecatedFile = path.join(this.ccsClaudeDir, 'agents', 'ccs-delegator.md');
158
+ const userSymlinkFile = path.join(this.homeDir, '.claude', 'agents', 'ccs-delegator.md');
159
+ const migrationMarker = path.join(this.homeDir, '.ccs', '.migrations', 'v435-delegator-cleanup');
160
+
161
+ let cleanedFiles = [];
162
+
163
+ try {
164
+ // Check if cleanup already done
165
+ if (fs.existsSync(migrationMarker)) {
166
+ return { success: true, cleanedFiles: [] }; // Already cleaned
167
+ }
168
+
169
+ // Clean up user symlink in ~/.claude/agents/ccs-delegator.md FIRST
170
+ // This ensures we can detect broken symlinks before deleting the target
171
+ try {
172
+ const userStats = fs.lstatSync(userSymlinkFile);
173
+ if (userStats.isSymbolicLink()) {
174
+ fs.unlinkSync(userSymlinkFile);
175
+ cleanedFiles.push('user symlink');
176
+ } else {
177
+ // It's not a symlink (user created their own file), backup it
178
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
179
+ const backupPath = `${userSymlinkFile}.backup-${timestamp}`;
180
+ fs.renameSync(userSymlinkFile, backupPath);
181
+ if (!silent) console.log(`[i] Backed up user file to ${path.basename(backupPath)}`);
182
+ cleanedFiles.push('user file (backed up)');
183
+ }
184
+ } catch (err) {
185
+ // File doesn't exist or other error - that's okay
186
+ if (err.code !== 'ENOENT' && !silent) {
187
+ console.log(`[!] Failed to remove user symlink: ${err.message}`);
188
+ }
189
+ }
190
+
191
+ // Clean up package copy in ~/.ccs/.claude/agents/ccs-delegator.md
192
+ if (fs.existsSync(deprecatedFile)) {
193
+ try {
194
+ // Check if file was modified by user (compare with expected content)
195
+ const shouldBackup = this._shouldBackupDeprecatedFile(deprecatedFile);
196
+
197
+ if (shouldBackup) {
198
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
199
+ const backupPath = `${deprecatedFile}.backup-${timestamp}`;
200
+ fs.renameSync(deprecatedFile, backupPath);
201
+ if (!silent) console.log(`[i] Backed up modified deprecated file to ${path.basename(backupPath)}`);
202
+ } else {
203
+ fs.rmSync(deprecatedFile, { force: true });
204
+ }
205
+ cleanedFiles.push('package copy');
206
+ } catch (err) {
207
+ if (!silent) console.log(`[!] Failed to remove package copy: ${err.message}`);
208
+ }
209
+ }
210
+
211
+ // Create migration marker
212
+ if (cleanedFiles.length > 0) {
213
+ const migrationsDir = path.dirname(migrationMarker);
214
+ if (!fs.existsSync(migrationsDir)) {
215
+ fs.mkdirSync(migrationsDir, { recursive: true, mode: 0o700 });
216
+ }
217
+ fs.writeFileSync(migrationMarker, new Date().toISOString());
218
+
219
+ if (!silent) {
220
+ console.log(`[OK] Cleaned up deprecated agent files: ${cleanedFiles.join(', ')}`);
221
+ }
222
+ }
223
+
224
+ return { success: true, cleanedFiles };
225
+ } catch (err) {
226
+ if (!silent) console.log(`[!] Cleanup failed: ${err.message}`);
227
+ return { success: false, error: err.message, cleanedFiles };
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Check if deprecated file should be backed up (user modified)
233
+ * @param {string} filePath - Path to check
234
+ * @returns {boolean} True if file should be backed up
235
+ * @private
236
+ */
237
+ _shouldBackupDeprecatedFile(filePath) {
238
+ try {
239
+ // Simple heuristic: if file size differs significantly from expected, assume user modified
240
+ // Expected size for ccs-delegator.md was around 2-3KB
241
+ const stats = fs.statSync(filePath);
242
+ const expectedMinSize = 1000; // 1KB minimum
243
+ const expectedMaxSize = 10000; // 10KB maximum
244
+
245
+ // If size is outside expected range, likely user modified
246
+ return stats.size < expectedMinSize || stats.size > expectedMaxSize;
247
+ } catch (err) {
248
+ // If we can't determine, err on side of caution and backup
249
+ return true;
250
+ }
251
+ }
252
+
151
253
  /**
152
254
  * Check if ~/.ccs/.claude/ exists and is valid
153
255
  * @returns {boolean} True if directory exists
@@ -28,8 +28,7 @@ class ClaudeSymlinkManager {
28
28
  // CCS items to symlink (selective, item-level)
29
29
  this.ccsItems = [
30
30
  { source: 'commands/ccs', target: 'commands/ccs', type: 'directory' },
31
- { source: 'skills/ccs-delegation', target: 'skills/ccs-delegation', type: 'directory' },
32
- { source: 'agents/ccs-delegator.md', target: 'agents/ccs-delegator.md', type: 'file' }
31
+ { source: 'skills/ccs-delegation', target: 'skills/ccs-delegation', type: 'directory' }
33
32
  ];
34
33
  }
35
34
 
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.3.3"
5
+ CCS_VERSION="4.3.5"
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.3.3"
15
+ $CcsVersion = "4.3.5"
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": "4.3.3",
3
+ "version": "4.3.5",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
5
5
  "keywords": [
6
6
  "cli",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "cli-table3": "^0.6.5",
62
- "ora": "^5.4.1"
62
+ "ora": "^9.0.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "mocha": "^11.7.5"
@@ -109,6 +109,9 @@ function createConfigFiles() {
109
109
  const installer = new ClaudeDirInstaller();
110
110
  const packageDir = path.join(__dirname, '..');
111
111
  installer.install(packageDir);
112
+
113
+ // Clean up deprecated files (v4.3.2)
114
+ installer.cleanupDeprecated();
112
115
  } catch (err) {
113
116
  console.warn('[!] Failed to install .claude/ directory:', err.message);
114
117
  console.warn(' CCS items may not be available');