@kaitranntt/ccs 2.4.0 → 2.4.2

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 CHANGED
@@ -1 +1 @@
1
- 2.4.0
1
+ 2.4.2
package/bin/ccs.js CHANGED
@@ -5,12 +5,24 @@ const { spawn } = require('child_process');
5
5
  const path = require('path');
6
6
  const fs = require('fs');
7
7
  const { showError, colors } = require('./helpers');
8
- const { detectClaudeCli, validateClaudeCli, showClaudeNotFoundError } = require('./claude-detector');
8
+ const { detectClaudeCli, showClaudeNotFoundError } = require('./claude-detector');
9
9
  const { getSettingsPath } = require('./config-manager');
10
10
 
11
11
  // Version (sync with package.json)
12
12
  const CCS_VERSION = require('../package.json').version;
13
13
 
14
+ // Helper: Get spawn options for claude execution
15
+ // On Windows, .cmd/.bat/.ps1 files need shell: true
16
+ function getSpawnOptions(claudePath) {
17
+ const isWindows = process.platform === 'win32';
18
+ const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudePath);
19
+
20
+ return {
21
+ stdio: 'inherit',
22
+ shell: needsShell // Required for .cmd files on Windows
23
+ };
24
+ }
25
+
14
26
  // Special command handlers
15
27
  function handleVersionCommand() {
16
28
  console.log(`CCS (Claude Code Switch) version ${CCS_VERSION}`);
@@ -26,23 +38,17 @@ function handleVersionCommand() {
26
38
  }
27
39
 
28
40
  function handleHelpCommand(remainingArgs) {
29
- // Detect and validate Claude CLI
30
41
  const claudeCli = detectClaudeCli();
31
42
 
43
+ // Check if claude was found
32
44
  if (!claudeCli) {
33
45
  showClaudeNotFoundError();
34
46
  process.exit(1);
35
47
  }
36
48
 
37
- try {
38
- validateClaudeCli(claudeCli);
39
- } catch (e) {
40
- showError(e.message);
41
- process.exit(1);
42
- }
43
-
44
49
  // Execute claude --help
45
- const child = spawn(claudeCli, ['--help', ...remainingArgs], { stdio: 'inherit' });
50
+ const spawnOpts = getSpawnOptions(claudeCli);
51
+ const child = spawn(claudeCli, ['--help', ...remainingArgs], spawnOpts);
46
52
 
47
53
  child.on('exit', (code, signal) => {
48
54
  if (signal) {
@@ -53,7 +59,7 @@ function handleHelpCommand(remainingArgs) {
53
59
  });
54
60
 
55
61
  child.on('error', (err) => {
56
- console.error(`Error executing claude --help: ${err.message}`);
62
+ showClaudeNotFoundError();
57
63
  process.exit(1);
58
64
  });
59
65
  }
@@ -124,20 +130,15 @@ function main() {
124
130
  if (profile === 'default') {
125
131
  const claudeCli = detectClaudeCli();
126
132
 
133
+ // Check if claude was found
127
134
  if (!claudeCli) {
128
135
  showClaudeNotFoundError();
129
136
  process.exit(1);
130
137
  }
131
138
 
132
- try {
133
- validateClaudeCli(claudeCli);
134
- } catch (e) {
135
- showError(e.message);
136
- process.exit(1);
137
- }
138
-
139
139
  // Execute claude with args
140
- const child = spawn(claudeCli, remainingArgs, { stdio: 'inherit' });
140
+ const spawnOpts = getSpawnOptions(claudeCli);
141
+ const child = spawn(claudeCli, remainingArgs, spawnOpts);
141
142
 
142
143
  child.on('exit', (code, signal) => {
143
144
  if (signal) {
@@ -148,7 +149,7 @@ function main() {
148
149
  });
149
150
 
150
151
  child.on('error', (err) => {
151
- console.error(`Error executing claude: ${err.message}`);
152
+ showClaudeNotFoundError();
152
153
  process.exit(1);
153
154
  });
154
155
 
@@ -161,22 +162,16 @@ function main() {
161
162
  // Detect Claude CLI
162
163
  const claudeCli = detectClaudeCli();
163
164
 
165
+ // Check if claude was found
164
166
  if (!claudeCli) {
165
167
  showClaudeNotFoundError();
166
168
  process.exit(1);
167
169
  }
168
170
 
169
- // Validate Claude CLI path
170
- try {
171
- validateClaudeCli(claudeCli);
172
- } catch (e) {
173
- showError(e.message);
174
- process.exit(1);
175
- }
176
-
177
171
  // Execute claude with --settings
178
172
  const claudeArgs = ['--settings', settingsPath, ...remainingArgs];
179
- const child = spawn(claudeCli, claudeArgs, { stdio: 'inherit' });
173
+ const spawnOpts = getSpawnOptions(claudeCli);
174
+ const child = spawn(claudeCli, claudeArgs, spawnOpts);
180
175
 
181
176
  child.on('exit', (code, signal) => {
182
177
  if (signal) {
@@ -187,7 +182,7 @@ function main() {
187
182
  });
188
183
 
189
184
  child.on('error', (err) => {
190
- console.error(`Error executing claude: ${err.message}`);
185
+ showClaudeNotFoundError();
191
186
  process.exit(1);
192
187
  });
193
188
  }
@@ -1,156 +1,102 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const path = require('path');
5
4
  const { execSync } = require('child_process');
6
- const { showError, expandPath, isPathSafe } = require('./helpers');
5
+ const { showError, expandPath } = require('./helpers');
7
6
 
8
7
  // Detect Claude CLI executable
9
8
  function detectClaudeCli() {
10
- // Priority 1: CCS_CLAUDE_PATH environment variable
9
+ // Priority 1: CCS_CLAUDE_PATH environment variable (if user wants custom path)
11
10
  if (process.env.CCS_CLAUDE_PATH) {
12
11
  const ccsPath = expandPath(process.env.CCS_CLAUDE_PATH);
13
- if (fs.existsSync(ccsPath) && isExecutable(ccsPath)) {
12
+ // Basic validation: file exists
13
+ if (fs.existsSync(ccsPath)) {
14
14
  return ccsPath;
15
15
  }
16
- // Invalid CCS_CLAUDE_PATH - continue to fallbacks
16
+ // Invalid CCS_CLAUDE_PATH - show warning and fall back to PATH
17
+ console.warn('[!] Warning: CCS_CLAUDE_PATH is set but file not found:', ccsPath);
18
+ console.warn(' Falling back to system PATH lookup...');
17
19
  }
18
20
 
19
- // Priority 2: Check if claude in PATH
20
- try {
21
- const claudePath = execSync(
22
- process.platform === 'win32' ? 'where claude' : 'which claude',
23
- { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
24
- ).trim().split('\n')[0];
25
-
26
- if (claudePath && fs.existsSync(claudePath)) {
27
- return claudePath;
28
- }
29
- } catch (e) {
30
- // Not in PATH, continue to common locations
31
- }
32
-
33
- // Priority 3: Check common installation locations
34
- const commonLocations = getCommonLocations();
21
+ // Priority 2: Resolve 'claude' from PATH using which/where.exe
22
+ // This fixes Windows npm installation where spawn() can't resolve bare command names
23
+ // SECURITY: Commands are hardcoded literals with no user input - safe from injection
24
+ const isWindows = process.platform === 'win32';
35
25
 
36
- for (const location of commonLocations) {
37
- const expandedPath = expandPath(location);
38
- if (fs.existsSync(expandedPath) && isExecutable(expandedPath)) {
39
- return expandedPath;
26
+ try {
27
+ const cmd = isWindows ? 'where.exe claude' : 'which claude';
28
+ const result = execSync(cmd, {
29
+ encoding: 'utf8',
30
+ stdio: ['ignore', 'pipe', 'ignore'],
31
+ timeout: 5000 // 5 second timeout to prevent hangs
32
+ }).trim();
33
+
34
+ // where.exe may return multiple lines (all matches in PATH order)
35
+ const matches = result.split('\n').map(p => p.trim()).filter(p => p);
36
+
37
+ if (isWindows) {
38
+ // On Windows, prefer executables with extensions (.exe, .cmd, .bat)
39
+ // where.exe often returns file without extension first, then the actual .cmd wrapper
40
+ const withExtension = matches.find(p => /\.(exe|cmd|bat|ps1)$/i.test(p));
41
+ const claudePath = withExtension || matches[0];
42
+
43
+ if (claudePath && fs.existsSync(claudePath)) {
44
+ return claudePath;
45
+ }
46
+ } else {
47
+ // On Unix, first match is fine
48
+ const claudePath = matches[0];
49
+
50
+ if (claudePath && fs.existsSync(claudePath)) {
51
+ return claudePath;
52
+ }
40
53
  }
54
+ } catch (err) {
55
+ // Command failed - claude not in PATH
56
+ // Fall through to return null
41
57
  }
42
58
 
43
- // Not found
59
+ // Priority 3: Claude not found
44
60
  return null;
45
61
  }
46
62
 
47
- // Get platform-specific common locations
48
- function getCommonLocations() {
49
- const home = require('os').homedir();
50
-
51
- if (process.platform === 'win32') {
52
- return [
53
- path.join(process.env.LOCALAPPDATA || '', 'Claude', 'claude.exe'),
54
- path.join(process.env.PROGRAMFILES || '', 'Claude', 'claude.exe'),
55
- 'C:\\Program Files\\Claude\\claude.exe',
56
- 'D:\\Program Files\\Claude\\claude.exe',
57
- path.join(home, '.local', 'bin', 'claude.exe')
58
- ];
59
- } else if (process.platform === 'darwin') {
60
- return [
61
- '/usr/local/bin/claude',
62
- path.join(home, '.local/bin/claude'),
63
- '/opt/homebrew/bin/claude'
64
- ];
65
- } else {
66
- return [
67
- '/usr/local/bin/claude',
68
- path.join(home, '.local/bin/claude'),
69
- '/usr/bin/claude'
70
- ];
71
- }
72
- }
73
-
74
- // Check if file is executable
75
- function isExecutable(filePath) {
76
- try {
77
- fs.accessSync(filePath, fs.constants.X_OK);
78
- return true;
79
- } catch (e) {
80
- return false;
81
- }
82
- }
83
-
84
- // Validate Claude CLI path
85
- function validateClaudeCli(claudePath) {
86
- // Check 1: Empty path
87
- if (!claudePath) {
88
- throw new Error('No path provided');
89
- }
90
-
91
- // Check 2: File exists
92
- if (!fs.existsSync(claudePath)) {
93
- throw new Error(`File not found: ${claudePath}`);
94
- }
95
-
96
- // Check 3: Is regular file (not directory)
97
- const stats = fs.statSync(claudePath);
98
- if (!stats.isFile()) {
99
- throw new Error(`Path is a directory: ${claudePath}`);
100
- }
101
-
102
- // Check 4: Is executable
103
- if (!isExecutable(claudePath)) {
104
- throw new Error(`File is not executable: ${claudePath}\n\nTry: chmod +x ${claudePath}`);
105
- }
106
-
107
- // Check 5: Path safety (prevent injection)
108
- if (!isPathSafe(claudePath)) {
109
- throw new Error(`Path contains unsafe characters: ${claudePath}\n\nAllowed: alphanumeric, path separators, spaces, hyphens, underscores, dots`);
110
- }
111
-
112
- return true;
113
- }
114
-
115
- // Show Claude not found error
63
+ // Show Claude not found error with diagnostics
116
64
  function showClaudeNotFoundError() {
117
- const envVarStatus = process.env.CCS_CLAUDE_PATH || '(not set)';
118
65
  const isWindows = process.platform === 'win32';
66
+ const pathDirs = (process.env.PATH || '').split(isWindows ? ';' : ':');
67
+
68
+ const errorMsg = `Claude CLI not found in PATH
119
69
 
120
- const errorMsg = `Claude CLI not found
70
+ CCS requires Claude CLI to be installed and available in your PATH.
121
71
 
122
- Searched:
123
- - CCS_CLAUDE_PATH: ${envVarStatus}
124
- - System PATH: not found
125
- - Common locations: not found
72
+ [i] Diagnostic Info:
73
+ Platform: ${process.platform}
74
+ PATH directories: ${pathDirs.length}
75
+ Looking for: claude${isWindows ? '.exe' : ''}
126
76
 
127
77
  Solutions:
128
- 1. Add Claude CLI to PATH:
78
+ 1. Install Claude CLI:
79
+ https://docs.claude.com/en/docs/claude-code/installation
129
80
 
130
- ${isWindows
131
- ? '# Find where Claude is installed\n Get-ChildItem -Path C:\\,D:\\ -Filter claude.exe -Recurse\n\n # Add to PATH\n $env:Path += \';D:\\path\\to\\claude\\directory\'\n [Environment]::SetEnvironmentVariable(\'Path\', $env:Path, \'User\')'
132
- : '# Find where Claude is installed\n sudo find / -name claude 2>/dev/null\n\n # Add to PATH\n export PATH="/path/to/claude/bin:$PATH"\n echo \'export PATH="/path/to/claude/bin:$PATH"\' >> ~/.bashrc\n source ~/.bashrc'
133
- }
81
+ 2. Verify installation:
82
+ ${isWindows ? 'Get-Command claude' : 'command -v claude'}
134
83
 
135
- 2. Or set custom path:
84
+ 3. If installed but not in PATH, add it:
85
+ # Find Claude installation
86
+ ${isWindows ? 'where.exe claude' : 'which claude'}
136
87
 
88
+ # Or set custom path
137
89
  ${isWindows
138
- ? '$env:CCS_CLAUDE_PATH = \'D:\\full\\path\\to\\claude.exe\'\n [Environment]::SetEnvironmentVariable(\'CCS_CLAUDE_PATH\', \'D:\\full\\path\\to\\claude.exe\', \'User\')'
139
- : 'export CCS_CLAUDE_PATH="/full/path/to/claude"\n echo \'export CCS_CLAUDE_PATH="/full/path/to/claude"\' >> ~/.bashrc\n source ~/.bashrc'
90
+ ? '$env:CCS_CLAUDE_PATH = \'C:\\path\\to\\claude.exe\''
91
+ : 'export CCS_CLAUDE_PATH=\'/path/to/claude\''
140
92
  }
141
93
 
142
- 3. Or install Claude CLI:
143
-
144
- https://docs.claude.com/en/docs/claude-code/installation
145
-
146
- Verify installation:
147
- ccs --version`;
94
+ Restart your terminal after installation.`;
148
95
 
149
96
  showError(errorMsg);
150
97
  }
151
98
 
152
99
  module.exports = {
153
100
  detectClaudeCli,
154
- validateClaudeCli,
155
101
  showClaudeNotFoundError
156
102
  };
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="2.4.0"
5
+ CCS_VERSION="2.4.1"
6
6
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
7
 
8
8
  # --- Color/Format Functions ---
package/lib/ccs.ps1 CHANGED
@@ -72,7 +72,7 @@ Restart your terminal after installation.
72
72
  }
73
73
 
74
74
  # Version (updated by scripts/bump-version.sh)
75
- $CcsVersion = "2.4.0"
75
+ $CcsVersion = "2.4.1"
76
76
  $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
77
77
 
78
78
  # Installation function for commands and skills
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaitranntt/ccs",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
5
5
  "keywords": [
6
6
  "cli",