@kaitranntt/ccs 4.1.6 → 4.3.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/VERSION +1 -1
- package/bin/ccs.js +177 -2
- package/bin/management/doctor.js +202 -77
- package/bin/utils/claude-dir-installer.js +67 -9
- package/bin/utils/claude-symlink-manager.js +49 -19
- package/bin/utils/update-checker.js +243 -0
- package/lib/ccs +126 -1
- package/lib/ccs.ps1 +134 -1
- package/package.json +5 -1
- package/scripts/completion/ccs.bash +1 -1
- package/scripts/completion/ccs.fish +10 -9
- package/scripts/completion/ccs.ps1 +1 -1
- package/scripts/completion/ccs.zsh +2 -1
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const { colored } = require('./helpers');
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* ClaudeSymlinkManager - Manages selective symlinks from ~/.ccs/.claude/ to ~/.claude/
|
|
@@ -35,41 +37,62 @@ class ClaudeSymlinkManager {
|
|
|
35
37
|
* Install CCS items to user's ~/.claude/ via selective symlinks
|
|
36
38
|
* Safe: backs up existing files before creating symlinks
|
|
37
39
|
*/
|
|
38
|
-
install() {
|
|
40
|
+
install(silent = false) {
|
|
41
|
+
const spinner = silent ? null : ora('Installing CCS items to ~/.claude/').start();
|
|
42
|
+
|
|
39
43
|
// Ensure ~/.ccs/.claude/ exists (should be shipped with package)
|
|
40
44
|
if (!fs.existsSync(this.ccsClaudeDir)) {
|
|
41
|
-
|
|
45
|
+
const msg = 'CCS .claude/ directory not found, skipping symlink installation';
|
|
46
|
+
if (spinner) {
|
|
47
|
+
spinner.warn(`[!] ${msg}`);
|
|
48
|
+
} else {
|
|
49
|
+
console.log(`[!] ${msg}`);
|
|
50
|
+
}
|
|
42
51
|
return;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
// Create ~/.claude/ if missing
|
|
46
55
|
if (!fs.existsSync(this.userClaudeDir)) {
|
|
47
|
-
|
|
56
|
+
if (!silent) {
|
|
57
|
+
if (spinner) spinner.text = 'Creating ~/.claude/ directory';
|
|
58
|
+
}
|
|
48
59
|
fs.mkdirSync(this.userClaudeDir, { recursive: true, mode: 0o700 });
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
// Install each CCS item
|
|
63
|
+
let installed = 0;
|
|
52
64
|
for (const item of this.ccsItems) {
|
|
53
|
-
|
|
65
|
+
if (!silent && spinner) {
|
|
66
|
+
spinner.text = `Installing ${item.target}...`;
|
|
67
|
+
}
|
|
68
|
+
const result = this._installItem(item, silent);
|
|
69
|
+
if (result) installed++;
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
|
|
72
|
+
const msg = `${installed}/${this.ccsItems.length} items installed to ~/.claude/`;
|
|
73
|
+
if (spinner) {
|
|
74
|
+
spinner.succeed(colored('[OK]', 'green') + ` ${msg}`);
|
|
75
|
+
} else {
|
|
76
|
+
console.log(`[OK] ${msg}`);
|
|
77
|
+
}
|
|
57
78
|
}
|
|
58
79
|
|
|
59
80
|
/**
|
|
60
81
|
* Install a single CCS item with conflict handling
|
|
61
82
|
* @param {Object} item - Item descriptor {source, target, type}
|
|
83
|
+
* @param {boolean} silent - Suppress individual item messages
|
|
84
|
+
* @returns {boolean} True if installed successfully
|
|
62
85
|
* @private
|
|
63
86
|
*/
|
|
64
|
-
_installItem(item) {
|
|
87
|
+
_installItem(item, silent = false) {
|
|
65
88
|
const sourcePath = path.join(this.ccsClaudeDir, item.source);
|
|
66
89
|
const targetPath = path.join(this.userClaudeDir, item.target);
|
|
67
90
|
const targetDir = path.dirname(targetPath);
|
|
68
91
|
|
|
69
92
|
// Ensure source exists
|
|
70
93
|
if (!fs.existsSync(sourcePath)) {
|
|
71
|
-
console.log(`[!] Source not found: ${item.source}, skipping`);
|
|
72
|
-
return;
|
|
94
|
+
if (!silent) console.log(`[!] Source not found: ${item.source}, skipping`);
|
|
95
|
+
return false;
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
// Create target parent directory if needed
|
|
@@ -81,26 +104,30 @@ class ClaudeSymlinkManager {
|
|
|
81
104
|
if (fs.existsSync(targetPath)) {
|
|
82
105
|
// Check if it's already the correct symlink
|
|
83
106
|
if (this._isOurSymlink(targetPath, sourcePath)) {
|
|
84
|
-
return; // Already correct,
|
|
107
|
+
return true; // Already correct, counts as success
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
// Backup existing file/directory
|
|
88
|
-
this._backupItem(targetPath);
|
|
111
|
+
this._backupItem(targetPath, silent);
|
|
89
112
|
}
|
|
90
113
|
|
|
91
114
|
// Create symlink
|
|
92
115
|
try {
|
|
93
116
|
const symlinkType = item.type === 'directory' ? 'dir' : 'file';
|
|
94
117
|
fs.symlinkSync(sourcePath, targetPath, symlinkType);
|
|
95
|
-
console.log(`[OK] Symlinked ${item.target}`);
|
|
118
|
+
if (!silent) console.log(`[OK] Symlinked ${item.target}`);
|
|
119
|
+
return true;
|
|
96
120
|
} catch (err) {
|
|
97
121
|
// Windows fallback: stub for now, full implementation in v4.2
|
|
98
122
|
if (process.platform === 'win32') {
|
|
99
|
-
|
|
100
|
-
|
|
123
|
+
if (!silent) {
|
|
124
|
+
console.log(`[!] Symlink failed for ${item.target} (Windows fallback deferred to v4.2)`);
|
|
125
|
+
console.log(`[i] Enable Developer Mode or wait for next update`);
|
|
126
|
+
}
|
|
101
127
|
} else {
|
|
102
|
-
console.log(`[!] Failed to symlink ${item.target}: ${err.message}`);
|
|
128
|
+
if (!silent) console.log(`[!] Failed to symlink ${item.target}: ${err.message}`);
|
|
103
129
|
}
|
|
130
|
+
return false;
|
|
104
131
|
}
|
|
105
132
|
}
|
|
106
133
|
|
|
@@ -131,9 +158,10 @@ class ClaudeSymlinkManager {
|
|
|
131
158
|
/**
|
|
132
159
|
* Backup existing item before replacing with symlink
|
|
133
160
|
* @param {string} itemPath - Path to item to backup
|
|
161
|
+
* @param {boolean} silent - Suppress backup messages
|
|
134
162
|
* @private
|
|
135
163
|
*/
|
|
136
|
-
_backupItem(itemPath) {
|
|
164
|
+
_backupItem(itemPath, silent = false) {
|
|
137
165
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
|
138
166
|
const backupPath = `${itemPath}.backup-${timestamp}`;
|
|
139
167
|
|
|
@@ -147,9 +175,9 @@ class ClaudeSymlinkManager {
|
|
|
147
175
|
}
|
|
148
176
|
|
|
149
177
|
fs.renameSync(itemPath, finalBackupPath);
|
|
150
|
-
console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`);
|
|
178
|
+
if (!silent) console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`);
|
|
151
179
|
} catch (err) {
|
|
152
|
-
console.log(`[!] Failed to backup ${itemPath}: ${err.message}`);
|
|
180
|
+
if (!silent) console.log(`[!] Failed to backup ${itemPath}: ${err.message}`);
|
|
153
181
|
throw err; // Don't proceed if backup fails
|
|
154
182
|
}
|
|
155
183
|
}
|
|
@@ -230,8 +258,10 @@ class ClaudeSymlinkManager {
|
|
|
230
258
|
* Same as install() but with explicit sync message
|
|
231
259
|
*/
|
|
232
260
|
sync() {
|
|
233
|
-
console.log('
|
|
234
|
-
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log(colored('Syncing CCS Components...', 'cyan'));
|
|
263
|
+
console.log('');
|
|
264
|
+
this.install(false);
|
|
235
265
|
}
|
|
236
266
|
}
|
|
237
267
|
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const { colored } = require('./helpers');
|
|
8
|
+
|
|
9
|
+
const UPDATE_CHECK_FILE = path.join(os.homedir(), '.ccs', 'update-check.json');
|
|
10
|
+
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
|
11
|
+
const GITHUB_API_URL = 'https://api.github.com/repos/kaitranntt/ccs/releases/latest';
|
|
12
|
+
const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@kaitranntt/ccs/latest';
|
|
13
|
+
const REQUEST_TIMEOUT = 5000; // 5 seconds
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compare semantic versions
|
|
17
|
+
* @param {string} v1 - First version (e.g., "4.1.6")
|
|
18
|
+
* @param {string} v2 - Second version
|
|
19
|
+
* @returns {number} - 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
|
20
|
+
*/
|
|
21
|
+
function compareVersions(v1, v2) {
|
|
22
|
+
const parts1 = v1.replace(/^v/, '').split('.').map(Number);
|
|
23
|
+
const parts2 = v2.replace(/^v/, '').split('.').map(Number);
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
const p1 = parts1[i] || 0;
|
|
27
|
+
const p2 = parts2[i] || 0;
|
|
28
|
+
if (p1 > p2) return 1;
|
|
29
|
+
if (p1 < p2) return -1;
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fetch latest version from GitHub releases
|
|
36
|
+
* @returns {Promise<string|null>} - Latest version or null on error
|
|
37
|
+
*/
|
|
38
|
+
function fetchLatestVersionFromGitHub() {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const req = https.get(GITHUB_API_URL, {
|
|
41
|
+
headers: { 'User-Agent': 'CCS-Update-Checker' },
|
|
42
|
+
timeout: REQUEST_TIMEOUT
|
|
43
|
+
}, (res) => {
|
|
44
|
+
let data = '';
|
|
45
|
+
|
|
46
|
+
res.on('data', (chunk) => {
|
|
47
|
+
data += chunk;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
if (res.statusCode !== 200) {
|
|
53
|
+
resolve(null);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const release = JSON.parse(data);
|
|
58
|
+
const version = release.tag_name?.replace(/^v/, '') || null;
|
|
59
|
+
resolve(version);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
resolve(null);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
req.on('error', () => resolve(null));
|
|
67
|
+
req.on('timeout', () => {
|
|
68
|
+
req.destroy();
|
|
69
|
+
resolve(null);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch latest version from npm registry
|
|
76
|
+
* @returns {Promise<string|null>} - Latest version or null on error
|
|
77
|
+
*/
|
|
78
|
+
function fetchLatestVersionFromNpm() {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const req = https.get(NPM_REGISTRY_URL, {
|
|
81
|
+
headers: { 'User-Agent': 'CCS-Update-Checker' },
|
|
82
|
+
timeout: REQUEST_TIMEOUT
|
|
83
|
+
}, (res) => {
|
|
84
|
+
let data = '';
|
|
85
|
+
|
|
86
|
+
res.on('data', (chunk) => {
|
|
87
|
+
data += chunk;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
res.on('end', () => {
|
|
91
|
+
try {
|
|
92
|
+
if (res.statusCode !== 200) {
|
|
93
|
+
resolve(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const packageData = JSON.parse(data);
|
|
98
|
+
const version = packageData.version || null;
|
|
99
|
+
resolve(version);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
resolve(null);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
req.on('error', () => resolve(null));
|
|
107
|
+
req.on('timeout', () => {
|
|
108
|
+
req.destroy();
|
|
109
|
+
resolve(null);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Read update check cache
|
|
116
|
+
* @returns {Object} - Cache object
|
|
117
|
+
*/
|
|
118
|
+
function readCache() {
|
|
119
|
+
try {
|
|
120
|
+
if (!fs.existsSync(UPDATE_CHECK_FILE)) {
|
|
121
|
+
return { last_check: 0, latest_version: null, dismissed_version: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const data = fs.readFileSync(UPDATE_CHECK_FILE, 'utf8');
|
|
125
|
+
return JSON.parse(data);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return { last_check: 0, latest_version: null, dismissed_version: null };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Write update check cache
|
|
133
|
+
* @param {Object} cache - Cache object to write
|
|
134
|
+
*/
|
|
135
|
+
function writeCache(cache) {
|
|
136
|
+
try {
|
|
137
|
+
const ccsDir = path.join(os.homedir(), '.ccs');
|
|
138
|
+
if (!fs.existsSync(ccsDir)) {
|
|
139
|
+
fs.mkdirSync(ccsDir, { recursive: true, mode: 0o700 });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify(cache, null, 2), 'utf8');
|
|
143
|
+
} catch (err) {
|
|
144
|
+
// Silently fail - not critical
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check for updates (async, non-blocking)
|
|
150
|
+
* @param {string} currentVersion - Current CCS version
|
|
151
|
+
* @param {boolean} force - Force check even if within interval
|
|
152
|
+
* @param {string} installMethod - Installation method ('npm' or 'direct')
|
|
153
|
+
* @returns {Promise<Object>} - Update result object with status and data
|
|
154
|
+
*/
|
|
155
|
+
async function checkForUpdates(currentVersion, force = false, installMethod = 'direct') {
|
|
156
|
+
const cache = readCache();
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
|
|
159
|
+
// Check if we should check for updates
|
|
160
|
+
if (!force && (now - cache.last_check < CHECK_INTERVAL)) {
|
|
161
|
+
// Use cached result if available
|
|
162
|
+
if (cache.latest_version && compareVersions(cache.latest_version, currentVersion) > 0) {
|
|
163
|
+
// Don't show if user dismissed this version
|
|
164
|
+
if (cache.dismissed_version === cache.latest_version) {
|
|
165
|
+
return { status: 'no_update', reason: 'dismissed' };
|
|
166
|
+
}
|
|
167
|
+
return { status: 'update_available', latest: cache.latest_version, current: currentVersion };
|
|
168
|
+
}
|
|
169
|
+
return { status: 'no_update', reason: 'cached' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fetch latest version from appropriate source
|
|
173
|
+
let latestVersion;
|
|
174
|
+
let fetchError = null;
|
|
175
|
+
|
|
176
|
+
if (installMethod === 'npm') {
|
|
177
|
+
latestVersion = await fetchLatestVersionFromNpm();
|
|
178
|
+
if (!latestVersion) fetchError = 'npm_registry_error';
|
|
179
|
+
} else {
|
|
180
|
+
latestVersion = await fetchLatestVersionFromGitHub();
|
|
181
|
+
if (!latestVersion) fetchError = 'github_api_error';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Update cache
|
|
185
|
+
cache.last_check = now;
|
|
186
|
+
if (latestVersion) {
|
|
187
|
+
cache.latest_version = latestVersion;
|
|
188
|
+
}
|
|
189
|
+
writeCache(cache);
|
|
190
|
+
|
|
191
|
+
// Handle fetch errors
|
|
192
|
+
if (fetchError) {
|
|
193
|
+
return {
|
|
194
|
+
status: 'check_failed',
|
|
195
|
+
reason: fetchError,
|
|
196
|
+
message: `Failed to check for updates: ${fetchError.replace(/_/g, ' ')}`
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if update available
|
|
201
|
+
if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
|
|
202
|
+
// Don't show if user dismissed this version
|
|
203
|
+
if (cache.dismissed_version === latestVersion) {
|
|
204
|
+
return { status: 'no_update', reason: 'dismissed' };
|
|
205
|
+
}
|
|
206
|
+
return { status: 'update_available', latest: latestVersion, current: currentVersion };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { status: 'no_update', reason: 'latest' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Show update notification
|
|
214
|
+
* @param {Object} updateInfo - Update information
|
|
215
|
+
*/
|
|
216
|
+
function showUpdateNotification(updateInfo) {
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(colored('═══════════════════════════════════════════════════════', 'cyan'));
|
|
219
|
+
console.log(colored(` Update available: ${updateInfo.current} → ${updateInfo.latest}`, 'yellow'));
|
|
220
|
+
console.log(colored('═══════════════════════════════════════════════════════', 'cyan'));
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log(` Run ${colored('ccs update', 'yellow')} to update`);
|
|
223
|
+
console.log('');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Dismiss update notification for a specific version
|
|
228
|
+
* @param {string} version - Version to dismiss
|
|
229
|
+
*/
|
|
230
|
+
function dismissUpdate(version) {
|
|
231
|
+
const cache = readCache();
|
|
232
|
+
cache.dismissed_version = version;
|
|
233
|
+
writeCache(cache);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
compareVersions,
|
|
238
|
+
checkForUpdates,
|
|
239
|
+
showUpdateNotification,
|
|
240
|
+
dismissUpdate,
|
|
241
|
+
readCache,
|
|
242
|
+
writeCache
|
|
243
|
+
};
|
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.
|
|
5
|
+
CCS_VERSION="4.3.0"
|
|
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"
|
|
@@ -199,6 +199,7 @@ show_help() {
|
|
|
199
199
|
echo -e "${CYAN}Diagnostics:${RESET}"
|
|
200
200
|
echo -e " ${YELLOW}ccs doctor${RESET} Run health check and diagnostics"
|
|
201
201
|
echo -e " ${YELLOW}ccs sync${RESET} Sync delegation commands and skills"
|
|
202
|
+
echo -e " ${YELLOW}ccs update${RESET} Update CCS to latest version"
|
|
202
203
|
echo ""
|
|
203
204
|
|
|
204
205
|
echo -e "${CYAN}Flags:${RESET}"
|
|
@@ -591,6 +592,124 @@ sync_run() {
|
|
|
591
592
|
echo ""
|
|
592
593
|
}
|
|
593
594
|
|
|
595
|
+
# --- Update Command ---
|
|
596
|
+
|
|
597
|
+
update_run() {
|
|
598
|
+
echo ""
|
|
599
|
+
echo -e "${CYAN}Checking for updates...${RESET}"
|
|
600
|
+
echo ""
|
|
601
|
+
|
|
602
|
+
# Detect installation method
|
|
603
|
+
local install_method="direct"
|
|
604
|
+
if command -v npm &>/dev/null && npm list -g @kaitranntt/ccs &>/dev/null 2>&1; then
|
|
605
|
+
install_method="npm"
|
|
606
|
+
fi
|
|
607
|
+
|
|
608
|
+
# Fetch latest version from appropriate source
|
|
609
|
+
local latest_version=""
|
|
610
|
+
if command -v curl &>/dev/null; then
|
|
611
|
+
if [[ "$install_method" == "npm" ]]; then
|
|
612
|
+
# Check npm registry for npm installations
|
|
613
|
+
latest_version=$(curl -fsSL https://registry.npmjs.org/@kaitranntt/ccs/latest 2>/dev/null | \
|
|
614
|
+
grep '"version"' | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([0-9.]+)".*/\1/')
|
|
615
|
+
else
|
|
616
|
+
# Check GitHub releases for direct installations
|
|
617
|
+
latest_version=$(curl -fsSL https://api.github.com/repos/kaitranntt/ccs/releases/latest 2>/dev/null | \
|
|
618
|
+
grep '"tag_name"' | sed -E 's/.*"v?([0-9.]+)".*/\1/')
|
|
619
|
+
fi
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
if [[ -z "$latest_version" ]]; then
|
|
623
|
+
echo -e "${YELLOW}[!] Unable to check for updates${RESET}"
|
|
624
|
+
echo ""
|
|
625
|
+
echo "Try manually:"
|
|
626
|
+
if [[ "$install_method" == "npm" ]]; then
|
|
627
|
+
echo -e " ${YELLOW}npm install -g @kaitranntt/ccs@latest${RESET}"
|
|
628
|
+
else
|
|
629
|
+
echo -e " ${YELLOW}curl -fsSL ccs.kaitran.ca/install | bash${RESET}"
|
|
630
|
+
fi
|
|
631
|
+
echo ""
|
|
632
|
+
exit 1
|
|
633
|
+
fi
|
|
634
|
+
|
|
635
|
+
# Compare versions
|
|
636
|
+
if [[ "$latest_version" == "$CCS_VERSION" ]]; then
|
|
637
|
+
echo -e "${GREEN}[OK] You are already on the latest version (${CCS_VERSION})${RESET}"
|
|
638
|
+
echo ""
|
|
639
|
+
exit 0
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
# Check if update available
|
|
643
|
+
local current_major=$(echo "$CCS_VERSION" | cut -d. -f1)
|
|
644
|
+
local current_minor=$(echo "$CCS_VERSION" | cut -d. -f2)
|
|
645
|
+
local current_patch=$(echo "$CCS_VERSION" | cut -d. -f3)
|
|
646
|
+
|
|
647
|
+
local latest_major=$(echo "$latest_version" | cut -d. -f1)
|
|
648
|
+
local latest_minor=$(echo "$latest_version" | cut -d. -f2)
|
|
649
|
+
local latest_patch=$(echo "$latest_version" | cut -d. -f3)
|
|
650
|
+
|
|
651
|
+
local is_newer=0
|
|
652
|
+
if [[ $latest_major -gt $current_major ]]; then
|
|
653
|
+
is_newer=1
|
|
654
|
+
elif [[ $latest_major -eq $current_major ]] && [[ $latest_minor -gt $current_minor ]]; then
|
|
655
|
+
is_newer=1
|
|
656
|
+
elif [[ $latest_major -eq $current_major ]] && [[ $latest_minor -eq $current_minor ]] && [[ $latest_patch -gt $current_patch ]]; then
|
|
657
|
+
is_newer=1
|
|
658
|
+
fi
|
|
659
|
+
|
|
660
|
+
if [[ $is_newer -eq 0 ]]; then
|
|
661
|
+
echo -e "${GREEN}[OK] You are on version ${CCS_VERSION} (latest is ${latest_version})${RESET}"
|
|
662
|
+
echo ""
|
|
663
|
+
exit 0
|
|
664
|
+
fi
|
|
665
|
+
|
|
666
|
+
echo -e "${YELLOW}[i] Update available: ${CCS_VERSION} → ${latest_version}${RESET}"
|
|
667
|
+
echo ""
|
|
668
|
+
|
|
669
|
+
# Perform update based on installation method
|
|
670
|
+
if [[ "$install_method" == "npm" ]]; then
|
|
671
|
+
echo -e "${CYAN}Updating via npm...${RESET}"
|
|
672
|
+
echo ""
|
|
673
|
+
|
|
674
|
+
if npm install -g @kaitranntt/ccs@latest; then
|
|
675
|
+
echo ""
|
|
676
|
+
echo -e "${GREEN}[OK] Update successful!${RESET}"
|
|
677
|
+
echo ""
|
|
678
|
+
echo -e "Run ${YELLOW}ccs --version${RESET} to verify"
|
|
679
|
+
echo ""
|
|
680
|
+
exit 0
|
|
681
|
+
else
|
|
682
|
+
echo ""
|
|
683
|
+
echo -e "${RED}[X] Update failed${RESET}"
|
|
684
|
+
echo ""
|
|
685
|
+
echo "Try manually:"
|
|
686
|
+
echo -e " ${YELLOW}npm install -g @kaitranntt/ccs@latest${RESET}"
|
|
687
|
+
echo ""
|
|
688
|
+
exit 1
|
|
689
|
+
fi
|
|
690
|
+
else
|
|
691
|
+
echo -e "${CYAN}Updating via installer...${RESET}"
|
|
692
|
+
echo ""
|
|
693
|
+
|
|
694
|
+
if curl -fsSL ccs.kaitran.ca/install | bash; then
|
|
695
|
+
echo ""
|
|
696
|
+
echo -e "${GREEN}[OK] Update successful!${RESET}"
|
|
697
|
+
echo ""
|
|
698
|
+
echo -e "Run ${YELLOW}ccs --version${RESET} to verify"
|
|
699
|
+
echo ""
|
|
700
|
+
exit 0
|
|
701
|
+
else
|
|
702
|
+
echo ""
|
|
703
|
+
echo -e "${RED}[X] Update failed${RESET}"
|
|
704
|
+
echo ""
|
|
705
|
+
echo "Try manually:"
|
|
706
|
+
echo -e " ${YELLOW}curl -fsSL ccs.kaitran.ca/install | bash${RESET}"
|
|
707
|
+
echo ""
|
|
708
|
+
exit 1
|
|
709
|
+
fi
|
|
710
|
+
fi
|
|
711
|
+
}
|
|
712
|
+
|
|
594
713
|
# --- Claude CLI Detection Logic ---
|
|
595
714
|
|
|
596
715
|
detect_claude_cli() {
|
|
@@ -1669,6 +1788,12 @@ if [[ $# -gt 0 ]] && [[ "${1}" == "sync" || "${1}" == "--sync" ]]; then
|
|
|
1669
1788
|
exit $?
|
|
1670
1789
|
fi
|
|
1671
1790
|
|
|
1791
|
+
# Special case: update command
|
|
1792
|
+
if [[ $# -gt 0 ]] && [[ "${1}" == "update" || "${1}" == "--update" ]]; then
|
|
1793
|
+
update_run
|
|
1794
|
+
exit $?
|
|
1795
|
+
fi
|
|
1796
|
+
|
|
1672
1797
|
# Run auto-recovery before main logic
|
|
1673
1798
|
auto_recover || {
|
|
1674
1799
|
msg_error "Auto-recovery failed. Check permissions."
|