@kaitranntt/ccs 2.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/bin/ccs.js ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawn } = require('child_process');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const { showError, colors } = require('./helpers');
8
+ const { detectClaudeCli, validateClaudeCli, showClaudeNotFoundError } = require('./claude-detector');
9
+ const { getSettingsPath } = require('./config-manager');
10
+
11
+ // Version (sync with package.json)
12
+ const CCS_VERSION = require('../package.json').version;
13
+
14
+ // Special command handlers
15
+ function handleVersionCommand() {
16
+ console.log(`CCS (Claude Code Switch) version ${CCS_VERSION}`);
17
+
18
+ // Show install location
19
+ const installLocation = process.argv[1];
20
+ if (installLocation) {
21
+ console.log(`Installed at: ${installLocation}`);
22
+ }
23
+
24
+ console.log('https://github.com/kaitranntt/ccs');
25
+ process.exit(0);
26
+ }
27
+
28
+ function handleHelpCommand(remainingArgs) {
29
+ // Detect and validate Claude CLI
30
+ const claudeCli = detectClaudeCli();
31
+
32
+ if (!claudeCli) {
33
+ showClaudeNotFoundError();
34
+ process.exit(1);
35
+ }
36
+
37
+ try {
38
+ validateClaudeCli(claudeCli);
39
+ } catch (e) {
40
+ showError(e.message);
41
+ process.exit(1);
42
+ }
43
+
44
+ // Execute claude --help
45
+ const child = spawn(claudeCli, ['--help', ...remainingArgs], { stdio: 'inherit' });
46
+
47
+ child.on('exit', (code, signal) => {
48
+ if (signal) {
49
+ process.kill(process.pid, signal);
50
+ } else {
51
+ process.exit(code || 0);
52
+ }
53
+ });
54
+
55
+ child.on('error', (err) => {
56
+ console.error(`Error executing claude --help: ${err.message}`);
57
+ process.exit(1);
58
+ });
59
+ }
60
+
61
+ function handleInstallCommand() {
62
+ // Implementation for --install (copy commands/skills to ~/.claude)
63
+ console.log('[Installing CCS Commands and Skills]');
64
+ console.log('Feature not yet implemented in Node.js standalone');
65
+ console.log('Use traditional installer for now:');
66
+ console.log(process.platform === 'win32'
67
+ ? ' irm ccs.kaitran.ca/install | iex'
68
+ : ' curl -fsSL ccs.kaitran.ca/install | bash');
69
+ process.exit(0);
70
+ }
71
+
72
+ function handleUninstallCommand() {
73
+ // Implementation for --uninstall (remove commands/skills from ~/.claude)
74
+ console.log('[Uninstalling CCS Commands and Skills]');
75
+ console.log('Feature not yet implemented in Node.js standalone');
76
+ console.log('Use traditional uninstaller for now');
77
+ process.exit(0);
78
+ }
79
+
80
+ // Smart profile detection
81
+ function detectProfile(args) {
82
+ if (args.length === 0 || args[0].startsWith('-')) {
83
+ // No args or first arg is a flag → use default profile
84
+ return { profile: 'default', remainingArgs: args };
85
+ } else {
86
+ // First arg doesn't start with '-' → treat as profile name
87
+ return { profile: args[0], remainingArgs: args.slice(1) };
88
+ }
89
+ }
90
+
91
+ // Main execution
92
+ function main() {
93
+ const args = process.argv.slice(2);
94
+
95
+ // Special case: version command (check BEFORE profile detection)
96
+ const firstArg = args[0];
97
+ if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
98
+ handleVersionCommand();
99
+ }
100
+
101
+ // Special case: help command
102
+ if (firstArg === '--help' || firstArg === '-h' || firstArg === 'help') {
103
+ const remainingArgs = args.slice(1);
104
+ handleHelpCommand(remainingArgs);
105
+ return;
106
+ }
107
+
108
+ // Special case: install command
109
+ if (firstArg === '--install') {
110
+ handleInstallCommand();
111
+ return;
112
+ }
113
+
114
+ // Special case: uninstall command
115
+ if (firstArg === '--uninstall') {
116
+ handleUninstallCommand();
117
+ return;
118
+ }
119
+
120
+ // Detect profile
121
+ const { profile, remainingArgs } = detectProfile(args);
122
+
123
+ // Special case: "default" profile just runs claude directly
124
+ if (profile === 'default') {
125
+ const claudeCli = detectClaudeCli();
126
+
127
+ if (!claudeCli) {
128
+ showClaudeNotFoundError();
129
+ process.exit(1);
130
+ }
131
+
132
+ try {
133
+ validateClaudeCli(claudeCli);
134
+ } catch (e) {
135
+ showError(e.message);
136
+ process.exit(1);
137
+ }
138
+
139
+ // Execute claude with args
140
+ const child = spawn(claudeCli, remainingArgs, { stdio: 'inherit' });
141
+
142
+ child.on('exit', (code, signal) => {
143
+ if (signal) {
144
+ process.kill(process.pid, signal);
145
+ } else {
146
+ process.exit(code || 0);
147
+ }
148
+ });
149
+
150
+ child.on('error', (err) => {
151
+ console.error(`Error executing claude: ${err.message}`);
152
+ process.exit(1);
153
+ });
154
+
155
+ return;
156
+ }
157
+
158
+ // Get settings path for profile
159
+ const settingsPath = getSettingsPath(profile);
160
+
161
+ // Detect Claude CLI
162
+ const claudeCli = detectClaudeCli();
163
+
164
+ if (!claudeCli) {
165
+ showClaudeNotFoundError();
166
+ process.exit(1);
167
+ }
168
+
169
+ // Validate Claude CLI path
170
+ try {
171
+ validateClaudeCli(claudeCli);
172
+ } catch (e) {
173
+ showError(e.message);
174
+ process.exit(1);
175
+ }
176
+
177
+ // Execute claude with --settings
178
+ const claudeArgs = ['--settings', settingsPath, ...remainingArgs];
179
+ const child = spawn(claudeCli, claudeArgs, { stdio: 'inherit' });
180
+
181
+ child.on('exit', (code, signal) => {
182
+ if (signal) {
183
+ process.kill(process.pid, signal);
184
+ } else {
185
+ process.exit(code || 0);
186
+ }
187
+ });
188
+
189
+ child.on('error', (err) => {
190
+ console.error(`Error executing claude: ${err.message}`);
191
+ process.exit(1);
192
+ });
193
+ }
194
+
195
+ // Run main
196
+ main();
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+ const { showError, expandPath, isPathSafe } = require('./helpers');
7
+
8
+ // Detect Claude CLI executable
9
+ function detectClaudeCli() {
10
+ // Priority 1: CCS_CLAUDE_PATH environment variable
11
+ if (process.env.CCS_CLAUDE_PATH) {
12
+ const ccsPath = expandPath(process.env.CCS_CLAUDE_PATH);
13
+ if (fs.existsSync(ccsPath) && isExecutable(ccsPath)) {
14
+ return ccsPath;
15
+ }
16
+ // Invalid CCS_CLAUDE_PATH - continue to fallbacks
17
+ }
18
+
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();
35
+
36
+ for (const location of commonLocations) {
37
+ const expandedPath = expandPath(location);
38
+ if (fs.existsSync(expandedPath) && isExecutable(expandedPath)) {
39
+ return expandedPath;
40
+ }
41
+ }
42
+
43
+ // Not found
44
+ return null;
45
+ }
46
+
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
116
+ function showClaudeNotFoundError() {
117
+ const envVarStatus = process.env.CCS_CLAUDE_PATH || '(not set)';
118
+ const isWindows = process.platform === 'win32';
119
+
120
+ const errorMsg = `Claude CLI not found
121
+
122
+ Searched:
123
+ - CCS_CLAUDE_PATH: ${envVarStatus}
124
+ - System PATH: not found
125
+ - Common locations: not found
126
+
127
+ Solutions:
128
+ 1. Add Claude CLI to PATH:
129
+
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
+ }
134
+
135
+ 2. Or set custom path:
136
+
137
+ ${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'
140
+ }
141
+
142
+ 3. Or install Claude CLI:
143
+
144
+ https://docs.claude.com/en/docs/claude-code/installation
145
+
146
+ Verify installation:
147
+ ccs --version`;
148
+
149
+ showError(errorMsg);
150
+ }
151
+
152
+ module.exports = {
153
+ detectClaudeCli,
154
+ validateClaudeCli,
155
+ showClaudeNotFoundError
156
+ };
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { showError, expandPath, validateProfileName } = require('./helpers');
7
+
8
+ // Get config file path
9
+ function getConfigPath() {
10
+ return process.env.CCS_CONFIG || path.join(os.homedir(), '.ccs', 'config.json');
11
+ }
12
+
13
+ // Read and parse config
14
+ function readConfig() {
15
+ const configPath = getConfigPath();
16
+
17
+ // Check config exists
18
+ if (!fs.existsSync(configPath)) {
19
+ const isWindows = process.platform === 'win32';
20
+ showError(`Config file not found: ${configPath}
21
+
22
+ Solutions:
23
+ 1. Reinstall CCS:
24
+ ${isWindows ? 'irm ccs.kaitran.ca/install | iex' : 'curl -fsSL ccs.kaitran.ca/install | bash'}
25
+
26
+ 2. Or create config manually:
27
+ mkdir -p ~/.ccs
28
+ cat > ~/.ccs/config.json << 'EOF'
29
+ {
30
+ "profiles": {
31
+ "glm": "~/.ccs/glm.settings.json",
32
+ "default": "~/.claude/settings.json"
33
+ }
34
+ }
35
+ EOF`);
36
+ process.exit(1);
37
+ }
38
+
39
+ // Read and parse JSON
40
+ let config;
41
+ try {
42
+ const configContent = fs.readFileSync(configPath, 'utf8');
43
+ config = JSON.parse(configContent);
44
+ } catch (e) {
45
+ const isWindows = process.platform === 'win32';
46
+ showError(`Invalid JSON in ${configPath}
47
+
48
+ Fix the JSON syntax or reinstall:
49
+ ${isWindows ? 'irm ccs.kaitran.ca/install | iex' : 'curl -fsSL ccs.kaitran.ca/install | bash'}`);
50
+ process.exit(1);
51
+ }
52
+
53
+ // Validate config has profiles object
54
+ if (!config.profiles || typeof config.profiles !== 'object') {
55
+ const isWindows = process.platform === 'win32';
56
+ showError(`Config must have 'profiles' object
57
+
58
+ See config.example.json for correct format
59
+ Or reinstall:
60
+ ${isWindows ? 'irm ccs.kaitran.ca/install | iex' : 'curl -fsSL ccs.kaitran.ca/install | bash'}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ return config;
65
+ }
66
+
67
+ // Get settings path for profile
68
+ function getSettingsPath(profile) {
69
+ const config = readConfig();
70
+
71
+ // Validate profile name
72
+ if (!validateProfileName(profile)) {
73
+ showError(`Invalid profile name: ${profile}
74
+
75
+ Use only alphanumeric characters, dash, or underscore.`);
76
+ process.exit(1);
77
+ }
78
+
79
+ // Get settings path
80
+ const settingsPath = config.profiles[profile];
81
+
82
+ if (!settingsPath) {
83
+ const availableProfiles = Object.keys(config.profiles).map(p => ` - ${p}`).join('\n');
84
+ showError(`Profile '${profile}' not found in ${getConfigPath()}
85
+
86
+ Available profiles:
87
+ ${availableProfiles}`);
88
+ process.exit(1);
89
+ }
90
+
91
+ // Expand path
92
+ const expandedPath = expandPath(settingsPath);
93
+
94
+ // Validate settings file exists
95
+ if (!fs.existsSync(expandedPath)) {
96
+ const isWindows = process.platform === 'win32';
97
+ showError(`Settings file not found: ${expandedPath}
98
+
99
+ Solutions:
100
+ 1. Create the settings file for profile '${profile}'
101
+ 2. Update the path in ${getConfigPath()}
102
+ 3. Or reinstall: ${isWindows ? 'irm ccs.kaitran.ca/install | iex' : 'curl -fsSL ccs.kaitran.ca/install | bash'}`);
103
+ process.exit(1);
104
+ }
105
+
106
+ // Validate settings file is valid JSON
107
+ try {
108
+ const settingsContent = fs.readFileSync(expandedPath, 'utf8');
109
+ JSON.parse(settingsContent);
110
+ } catch (e) {
111
+ showError(`Invalid JSON in ${expandedPath}
112
+
113
+ Details: ${e.message}
114
+
115
+ Solutions:
116
+ 1. Validate JSON at https://jsonlint.com
117
+ 2. Or reset to template: echo '{"env":{}}' > ${expandedPath}
118
+ 3. Or reinstall CCS`);
119
+ process.exit(1);
120
+ }
121
+
122
+ return expandedPath;
123
+ }
124
+
125
+ module.exports = {
126
+ getConfigPath,
127
+ readConfig,
128
+ getSettingsPath
129
+ };
package/bin/helpers.js ADDED
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ // Color formatting (TTY-aware)
8
+ const useColors = process.stderr.isTTY && !process.env.NO_COLOR;
9
+ const colors = useColors ? {
10
+ red: '\x1b[0;31m',
11
+ yellow: '\x1b[1;33m',
12
+ cyan: '\x1b[0;36m',
13
+ green: '\x1b[0;32m',
14
+ bold: '\x1b[1m',
15
+ reset: '\x1b[0m'
16
+ } : { red: '', yellow: '', cyan: '', green: '', bold: '', reset: '' };
17
+
18
+ // Error formatting
19
+ function showError(message) {
20
+ console.error('');
21
+ console.error(colors.red + colors.bold + '╔═════════════════════════════════════════════╗' + colors.reset);
22
+ console.error(colors.red + colors.bold + '║ ERROR ║' + colors.reset);
23
+ console.error(colors.red + colors.bold + '╚═════════════════════════════════════════════╝' + colors.reset);
24
+ console.error('');
25
+ console.error(colors.red + message + colors.reset);
26
+ console.error('');
27
+ }
28
+
29
+ // Path expansion (~ and env vars)
30
+ function expandPath(pathStr) {
31
+ // Handle tilde expansion
32
+ if (pathStr.startsWith('~/') || pathStr.startsWith('~\\')) {
33
+ pathStr = path.join(os.homedir(), pathStr.slice(2));
34
+ }
35
+
36
+ // Expand environment variables (Windows and Unix)
37
+ pathStr = pathStr.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] || '');
38
+ pathStr = pathStr.replace(/\$([A-Z_][A-Z0-9_]*)/gi, (_, name) => process.env[name] || '');
39
+
40
+ // Windows %VAR% style
41
+ if (process.platform === 'win32') {
42
+ pathStr = pathStr.replace(/%([^%]+)%/g, (_, name) => process.env[name] || '');
43
+ }
44
+
45
+ return path.normalize(pathStr);
46
+ }
47
+
48
+ // Validate profile name (alphanumeric, dash, underscore only)
49
+ function validateProfileName(profile) {
50
+ return /^[a-zA-Z0-9_-]+$/.test(profile);
51
+ }
52
+
53
+ // Validate path safety (prevent injection)
54
+ function isPathSafe(pathStr) {
55
+ // Allow: alphanumeric, path separators, space, dash, underscore, dot, colon, tilde
56
+ return !/[;|&<>`$*?\[\]'"()]/.test(pathStr);
57
+ }
58
+
59
+ module.exports = {
60
+ colors,
61
+ showError,
62
+ expandPath,
63
+ validateProfileName,
64
+ isPathSafe
65
+ };
@@ -0,0 +1,10 @@
1
+ {
2
+ "env": {
3
+ "ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
4
+ "ANTHROPIC_AUTH_TOKEN": "YOUR_GLM_API_KEY_HERE",
5
+ "ANTHROPIC_MODEL": "glm-4.6",
6
+ "ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
7
+ "ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
8
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
9
+ }
10
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "profiles": {
3
+ "glm": "~/.ccs/glm.settings.json",
4
+ "default": "~/.claude/settings.json"
5
+ }
6
+ }