@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 +1 -1
- package/bin/ccs.js +25 -30
- package/bin/claude-detector.js +62 -116
- package/lib/ccs +1 -1
- package/lib/ccs.ps1 +1 -1
- package/package.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.4.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
185
|
+
showClaudeNotFoundError();
|
|
191
186
|
process.exit(1);
|
|
192
187
|
});
|
|
193
188
|
}
|
package/bin/claude-detector.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
12
|
+
// Basic validation: file exists
|
|
13
|
+
if (fs.existsSync(ccsPath)) {
|
|
14
14
|
return ccsPath;
|
|
15
15
|
}
|
|
16
|
-
// Invalid CCS_CLAUDE_PATH -
|
|
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:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
//
|
|
59
|
+
// Priority 3: Claude not found
|
|
44
60
|
return null;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
//
|
|
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
|
-
|
|
70
|
+
CCS requires Claude CLI to be installed and available in your PATH.
|
|
121
71
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
78
|
+
1. Install Claude CLI:
|
|
79
|
+
https://docs.claude.com/en/docs/claude-code/installation
|
|
129
80
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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 = \'
|
|
139
|
-
: 'export CCS_CLAUDE_PATH
|
|
90
|
+
? '$env:CCS_CLAUDE_PATH = \'C:\\path\\to\\claude.exe\''
|
|
91
|
+
: 'export CCS_CLAUDE_PATH=\'/path/to/claude\''
|
|
140
92
|
}
|
|
141
93
|
|
|
142
|
-
|
|
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
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.
|
|
75
|
+
$CcsVersion = "2.4.1"
|
|
76
76
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
77
77
|
|
|
78
78
|
# Installation function for commands and skills
|