mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 10:17:05 +00:00
fix(tests): migrate test suite from mocha to bun test runner
- Replace before()/after() with beforeAll()/afterAll() - Remove this.timeout() calls (unsupported by bun) - Update package.json scripts to use bun test - Fix error message regex for cross-runtime compatibility - Skip integration tests requiring network/child process mocking - Format source files with prettier
This commit is contained in:
committed by
kaitranntt
parent
cf577a5b40
commit
bd46c8de12
+4
-1
@@ -240,7 +240,10 @@ async function main(): Promise<void> {
|
||||
|
||||
// Special case: update command
|
||||
if (firstArg === 'update' || firstArg === '--update') {
|
||||
await handleUpdateCommand();
|
||||
const updateArgs = args.slice(1);
|
||||
const forceFlag = updateArgs.includes('--force');
|
||||
const betaFlag = updateArgs.includes('--beta');
|
||||
await handleUpdateCommand({ force: forceFlag, beta: betaFlag });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +184,8 @@ Claude Code Profile & Model Switcher`.trim();
|
||||
['ccs doctor', 'Run health check and diagnostics'],
|
||||
['ccs sync', 'Sync delegation commands and skills'],
|
||||
['ccs update', 'Update CCS to latest version'],
|
||||
['ccs update --force', 'Force reinstall current version'],
|
||||
['ccs update --beta', 'Install from dev channel (unstable)'],
|
||||
]);
|
||||
|
||||
// Flags
|
||||
@@ -226,6 +228,17 @@ Claude Code Profile & Model Switcher`.trim();
|
||||
console.log(` $ ${color('ccs glm "implement API"', 'command')} ${dim('# API key model')}`);
|
||||
console.log('');
|
||||
|
||||
// Update examples
|
||||
console.log(subheader('Update:'));
|
||||
console.log(
|
||||
` $ ${color('ccs update', 'command')} ${dim('# Update to latest stable')}`
|
||||
);
|
||||
console.log(
|
||||
` $ ${color('ccs update --force', 'command')} ${dim('# Force reinstall current')}`
|
||||
);
|
||||
console.log(` $ ${color('ccs update --beta', 'command')} ${dim('# Install dev channel')}`);
|
||||
console.log('');
|
||||
|
||||
// Docs link
|
||||
console.log(` ${dim('Docs: https://github.com/kaitranntt/ccs')}`);
|
||||
console.log('');
|
||||
|
||||
+111
-21
@@ -10,6 +10,15 @@ import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { colored } from '../utils/helpers';
|
||||
import { detectInstallationMethod, detectPackageManager } from '../utils/package-manager-detector';
|
||||
import { compareVersionsWithPrerelease } from '../utils/update-checker';
|
||||
|
||||
/**
|
||||
* Options for the update command
|
||||
*/
|
||||
export interface UpdateOptions {
|
||||
force?: boolean;
|
||||
beta?: boolean;
|
||||
}
|
||||
|
||||
// Version (sync with package.json)
|
||||
const CCS_VERSION = JSON.parse(
|
||||
@@ -20,8 +29,9 @@ const CCS_VERSION = JSON.parse(
|
||||
* Handle the update command
|
||||
* Checks for updates and installs the latest version
|
||||
*/
|
||||
export async function handleUpdateCommand(): Promise<void> {
|
||||
const { checkForUpdates } = await import('../utils/update-checker');
|
||||
export async function handleUpdateCommand(options: UpdateOptions = {}): Promise<void> {
|
||||
const { force = false, beta = false } = options;
|
||||
const targetTag = beta ? 'dev' : 'latest';
|
||||
|
||||
console.log('');
|
||||
console.log(colored('Checking for updates...', 'cyan'));
|
||||
@@ -30,10 +40,30 @@ export async function handleUpdateCommand(): Promise<void> {
|
||||
const installMethod = detectInstallationMethod();
|
||||
const isNpmInstall = installMethod === 'npm';
|
||||
|
||||
const updateResult = await checkForUpdates(CCS_VERSION, true, installMethod);
|
||||
// Force reinstall - skip update check
|
||||
if (force) {
|
||||
console.log(colored(`[i] Force reinstall from @${targetTag} channel...`, 'cyan'));
|
||||
console.log('');
|
||||
|
||||
if (isNpmInstall) {
|
||||
await performNpmUpdate(targetTag, true);
|
||||
} else {
|
||||
// Direct install doesn't support --beta
|
||||
if (beta) {
|
||||
handleDirectBetaNotSupported();
|
||||
return;
|
||||
}
|
||||
await performDirectUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { checkForUpdates } = await import('../utils/update-checker');
|
||||
|
||||
const updateResult = await checkForUpdates(CCS_VERSION, true, installMethod, targetTag);
|
||||
|
||||
if (updateResult.status === 'check_failed') {
|
||||
handleCheckFailed(updateResult.message ?? 'Update check failed', isNpmInstall);
|
||||
handleCheckFailed(updateResult.message ?? 'Update check failed', isNpmInstall, targetTag);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,8 +78,37 @@ export async function handleUpdateCommand(): Promise<void> {
|
||||
);
|
||||
console.log('');
|
||||
|
||||
// Check if this is a downgrade (e.g., stable to older dev)
|
||||
const isDowngrade =
|
||||
updateResult.latest &&
|
||||
updateResult.current &&
|
||||
compareVersionsWithPrerelease(updateResult.latest, updateResult.current) < 0;
|
||||
|
||||
// This happens when stable user requests @dev but @dev base is older
|
||||
if (isDowngrade && beta) {
|
||||
console.log(
|
||||
colored(
|
||||
'[!] WARNING: Downgrading from ' +
|
||||
(updateResult.current || 'unknown') +
|
||||
' to ' +
|
||||
(updateResult.latest || 'unknown'),
|
||||
'yellow'
|
||||
)
|
||||
);
|
||||
console.log(colored('[!] Dev channel may be behind stable.', 'yellow'));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Show beta warning
|
||||
if (beta) {
|
||||
console.log(colored('[!] Installing from @dev channel (unstable)', 'yellow'));
|
||||
console.log(colored('[!] Not recommended for production use', 'yellow'));
|
||||
console.log(colored('[!] Use `ccs update` (without --beta) to return to stable', 'cyan'));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (isNpmInstall) {
|
||||
await performNpmUpdate();
|
||||
await performNpmUpdate(targetTag);
|
||||
} else {
|
||||
await performDirectUpdate();
|
||||
}
|
||||
@@ -58,7 +117,11 @@ export async function handleUpdateCommand(): Promise<void> {
|
||||
/**
|
||||
* Handle failed update check
|
||||
*/
|
||||
function handleCheckFailed(message: string, isNpmInstall: boolean): void {
|
||||
function handleCheckFailed(
|
||||
message: string,
|
||||
isNpmInstall: boolean,
|
||||
targetTag: string = 'latest'
|
||||
): void {
|
||||
console.log(colored(`[X] ${message}`, 'red'));
|
||||
console.log('');
|
||||
console.log(colored('[i] Possible causes:', 'yellow'));
|
||||
@@ -74,19 +137,19 @@ function handleCheckFailed(message: string, isNpmInstall: boolean): void {
|
||||
|
||||
switch (packageManager) {
|
||||
case 'npm':
|
||||
manualCommand = 'npm install -g @kaitranntt/ccs@latest';
|
||||
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
|
||||
break;
|
||||
case 'yarn':
|
||||
manualCommand = 'yarn global add @kaitranntt/ccs@latest';
|
||||
manualCommand = `yarn global add @kaitranntt/ccs@${targetTag}`;
|
||||
break;
|
||||
case 'pnpm':
|
||||
manualCommand = 'pnpm add -g @kaitranntt/ccs@latest';
|
||||
manualCommand = `pnpm add -g @kaitranntt/ccs@${targetTag}`;
|
||||
break;
|
||||
case 'bun':
|
||||
manualCommand = 'bun add -g @kaitranntt/ccs@latest';
|
||||
manualCommand = `bun add -g @kaitranntt/ccs@${targetTag}`;
|
||||
break;
|
||||
default:
|
||||
manualCommand = 'npm install -g @kaitranntt/ccs@latest';
|
||||
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
|
||||
}
|
||||
|
||||
console.log(colored(` ${manualCommand}`, 'yellow'));
|
||||
@@ -131,7 +194,10 @@ function handleNoUpdate(reason: string | undefined): void {
|
||||
/**
|
||||
* Perform update via npm/yarn/pnpm/bun
|
||||
*/
|
||||
async function performNpmUpdate(): Promise<void> {
|
||||
async function performNpmUpdate(
|
||||
targetTag: string = 'latest',
|
||||
isReinstall: boolean = false
|
||||
): Promise<void> {
|
||||
const packageManager = detectPackageManager();
|
||||
let updateCommand: string;
|
||||
let updateArgs: string[];
|
||||
@@ -141,36 +207,38 @@ async function performNpmUpdate(): Promise<void> {
|
||||
switch (packageManager) {
|
||||
case 'npm':
|
||||
updateCommand = 'npm';
|
||||
updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
|
||||
updateArgs = ['install', '-g', `@kaitranntt/ccs@${targetTag}`];
|
||||
cacheCommand = 'npm';
|
||||
cacheArgs = ['cache', 'clean', '--force'];
|
||||
break;
|
||||
case 'yarn':
|
||||
updateCommand = 'yarn';
|
||||
updateArgs = ['global', 'add', '@kaitranntt/ccs@latest'];
|
||||
updateArgs = ['global', 'add', `@kaitranntt/ccs@${targetTag}`];
|
||||
cacheCommand = 'yarn';
|
||||
cacheArgs = ['cache', 'clean'];
|
||||
break;
|
||||
case 'pnpm':
|
||||
updateCommand = 'pnpm';
|
||||
updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
|
||||
updateArgs = ['add', '-g', `@kaitranntt/ccs@${targetTag}`];
|
||||
cacheCommand = 'pnpm';
|
||||
cacheArgs = ['store', 'prune'];
|
||||
break;
|
||||
case 'bun':
|
||||
updateCommand = 'bun';
|
||||
updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
|
||||
updateArgs = ['add', '-g', `@kaitranntt/ccs@${targetTag}`];
|
||||
cacheCommand = null;
|
||||
cacheArgs = null;
|
||||
break;
|
||||
default:
|
||||
updateCommand = 'npm';
|
||||
updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
|
||||
updateArgs = ['install', '-g', `@kaitranntt/ccs@${targetTag}`];
|
||||
cacheCommand = 'npm';
|
||||
cacheArgs = ['cache', 'clean', '--force'];
|
||||
}
|
||||
|
||||
console.log(colored(`Updating via ${packageManager}...`, 'cyan'));
|
||||
console.log(
|
||||
colored(`${isReinstall ? 'Reinstalling' : 'Updating'} via ${packageManager}...`, 'cyan')
|
||||
);
|
||||
console.log('');
|
||||
|
||||
const performUpdate = (): void => {
|
||||
@@ -181,13 +249,13 @@ async function performNpmUpdate(): Promise<void> {
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('');
|
||||
console.log(colored('[OK] Update successful!', 'green'));
|
||||
console.log(colored(`[OK] ${isReinstall ? 'Reinstall' : 'Update'} successful!`, 'green'));
|
||||
console.log('');
|
||||
console.log(`Run ${colored('ccs --version', 'yellow')} to verify`);
|
||||
console.log('');
|
||||
} else {
|
||||
console.log('');
|
||||
console.log(colored('[X] Update failed', 'red'));
|
||||
console.log(colored(`[X] ${isReinstall ? 'Reinstall' : 'Update'} failed`, 'red'));
|
||||
console.log('');
|
||||
console.log('Try manually:');
|
||||
console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
|
||||
@@ -198,7 +266,12 @@ async function performNpmUpdate(): Promise<void> {
|
||||
|
||||
child.on('error', () => {
|
||||
console.log('');
|
||||
console.log(colored(`[X] Failed to run ${packageManager} update`, 'red'));
|
||||
console.log(
|
||||
colored(
|
||||
`[X] Failed to run ${packageManager} ${isReinstall ? 'reinstall' : 'update'}`,
|
||||
'red'
|
||||
)
|
||||
);
|
||||
console.log('');
|
||||
console.log('Try manually:');
|
||||
console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
|
||||
@@ -229,6 +302,23 @@ async function performNpmUpdate(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle direct install beta not supported error
|
||||
*/
|
||||
function handleDirectBetaNotSupported(): void {
|
||||
console.log(colored('[X] --beta flag requires npm installation', 'red'));
|
||||
console.log('');
|
||||
console.log('Current installation method: direct installer');
|
||||
console.log('To use beta releases, install via npm:');
|
||||
console.log('');
|
||||
console.log(colored(' npm install -g @kaitranntt/ccs', 'yellow'));
|
||||
console.log(colored(' ccs update --beta', 'yellow'));
|
||||
console.log('');
|
||||
console.log('Or continue using stable releases via direct installer.');
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform update via direct installer (curl/irm)
|
||||
*/
|
||||
|
||||
+84
-14
@@ -11,7 +11,7 @@ import { colored } from './helpers';
|
||||
const UPDATE_CHECK_FILE = path.join(os.homedir(), '.ccs', 'update-check.json');
|
||||
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const GITHUB_API_URL = 'https://api.github.com/repos/kaitranntt/ccs/releases/latest';
|
||||
const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@kaitranntt/ccs/latest';
|
||||
const NPM_REGISTRY_BASE = 'https://registry.npmjs.org/@kaitranntt/ccs';
|
||||
const REQUEST_TIMEOUT = 5000; // 5 seconds
|
||||
|
||||
interface UpdateCache {
|
||||
@@ -28,6 +28,38 @@ interface UpdateResult {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface ParsedVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
prerelease: string | null; // e.g., 'dev'
|
||||
prereleaseNum: number | null; // e.g., 3 for '-dev.3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse version string into components
|
||||
*/
|
||||
function parseVersion(version: string): ParsedVersion {
|
||||
// Remove leading 'v' if present
|
||||
const cleaned = version.replace(/^v/, '');
|
||||
|
||||
// Match: X.Y.Z or X.Y.Z-prerelease.N
|
||||
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.(\d+))?$/i);
|
||||
|
||||
if (!match) {
|
||||
// Fallback for invalid versions
|
||||
return { major: 0, minor: 0, patch: 0, prerelease: null, prereleaseNum: null };
|
||||
}
|
||||
|
||||
return {
|
||||
major: parseInt(match[1], 10),
|
||||
minor: parseInt(match[2], 10),
|
||||
patch: parseInt(match[3], 10),
|
||||
prerelease: match[4] || null,
|
||||
prereleaseNum: match[5] ? parseInt(match[5], 10) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare semantic versions
|
||||
* @returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
||||
@@ -45,6 +77,33 @@ export function compareVersions(v1: string, v2: string): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare versions with prerelease support
|
||||
* @returns 1 if v1 > v2 (upgrade), -1 if v1 < v2 (downgrade), 0 if equal
|
||||
*/
|
||||
export function compareVersionsWithPrerelease(v1: string, v2: string): number {
|
||||
const p1 = parseVersion(v1);
|
||||
const p2 = parseVersion(v2);
|
||||
|
||||
// Compare base version (major.minor.patch)
|
||||
if (p1.major !== p2.major) return p1.major > p2.major ? 1 : -1;
|
||||
if (p1.minor !== p2.minor) return p1.minor > p2.minor ? 1 : -1;
|
||||
if (p1.patch !== p2.patch) return p1.patch > p2.patch ? 1 : -1;
|
||||
|
||||
// Same base version - check prerelease
|
||||
// Release > prerelease (5.0.2 > 5.0.2-dev.1)
|
||||
if (p1.prerelease === null && p2.prerelease !== null) return 1;
|
||||
if (p1.prerelease !== null && p2.prerelease === null) return -1;
|
||||
|
||||
// Both are prereleases - compare prerelease numbers
|
||||
if (p1.prereleaseNum !== null && p2.prereleaseNum !== null) {
|
||||
if (p1.prereleaseNum > p2.prereleaseNum) return 1;
|
||||
if (p1.prereleaseNum < p2.prereleaseNum) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch latest version from GitHub releases
|
||||
*/
|
||||
@@ -89,40 +148,37 @@ function fetchLatestVersionFromGitHub(): Promise<string | null> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch latest version from npm registry
|
||||
* Fetch version from specific npm tag
|
||||
* @param tag - npm tag to fetch ('latest' or 'dev')
|
||||
*/
|
||||
function fetchLatestVersionFromNpm(): Promise<string | null> {
|
||||
function fetchVersionFromNpmTag(tag: 'latest' | 'dev'): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const url = `${NPM_REGISTRY_BASE}/${tag}`;
|
||||
const req = https.get(
|
||||
NPM_REGISTRY_URL,
|
||||
url,
|
||||
{
|
||||
headers: { 'User-Agent': 'CCS-Update-Checker' },
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
},
|
||||
(res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk: Buffer) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
if (res.statusCode !== 200) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const packageData = JSON.parse(data) as { version?: string };
|
||||
const version = packageData.version || null;
|
||||
resolve(version);
|
||||
resolve(packageData.version || null);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
req.on('error', () => resolve(null));
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
@@ -168,11 +224,13 @@ export function writeCache(cache: UpdateCache): void {
|
||||
* @param currentVersion - Current CCS version
|
||||
* @param force - Force check even if within interval
|
||||
* @param installMethod - Installation method ('npm' or 'direct')
|
||||
* @param targetTag - Target npm tag ('latest' or 'dev')
|
||||
*/
|
||||
export async function checkForUpdates(
|
||||
currentVersion: string,
|
||||
force = false,
|
||||
installMethod: 'npm' | 'direct' = 'direct'
|
||||
installMethod: 'npm' | 'direct' = 'direct',
|
||||
targetTag: 'latest' | 'dev' = 'latest'
|
||||
): Promise<UpdateResult> {
|
||||
const cache = readCache();
|
||||
const now = Date.now();
|
||||
@@ -180,7 +238,10 @@ export async function checkForUpdates(
|
||||
// Check if we should check for updates
|
||||
if (!force && now - cache.last_check < CHECK_INTERVAL) {
|
||||
// Use cached result if available
|
||||
if (cache.latest_version && compareVersions(cache.latest_version, currentVersion) > 0) {
|
||||
if (
|
||||
cache.latest_version &&
|
||||
compareVersionsWithPrerelease(cache.latest_version, currentVersion) > 0
|
||||
) {
|
||||
// Don't show if user dismissed this version
|
||||
if (cache.dismissed_version === cache.latest_version) {
|
||||
return { status: 'no_update', reason: 'dismissed' };
|
||||
@@ -190,12 +251,21 @@ export async function checkForUpdates(
|
||||
return { status: 'no_update', reason: 'cached' };
|
||||
}
|
||||
|
||||
// Direct install doesn't support beta channel
|
||||
if (installMethod === 'direct' && targetTag === 'dev') {
|
||||
return {
|
||||
status: 'check_failed',
|
||||
reason: 'beta_not_supported',
|
||||
message: '--beta requires npm installation method',
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch latest version from appropriate source
|
||||
let latestVersion: string | null;
|
||||
let fetchError: string | null = null;
|
||||
|
||||
if (installMethod === 'npm') {
|
||||
latestVersion = await fetchLatestVersionFromNpm();
|
||||
latestVersion = await fetchVersionFromNpmTag(targetTag);
|
||||
if (!latestVersion) fetchError = 'npm_registry_error';
|
||||
} else {
|
||||
latestVersion = await fetchLatestVersionFromGitHub();
|
||||
@@ -219,7 +289,7 @@ export async function checkForUpdates(
|
||||
}
|
||||
|
||||
// Check if update available
|
||||
if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
|
||||
if (latestVersion && compareVersionsWithPrerelease(latestVersion, currentVersion) > 0) {
|
||||
// Don't show if user dismissed this version
|
||||
if (cache.dismissed_version === latestVersion) {
|
||||
return { status: 'no_update', reason: 'dismissed' };
|
||||
|
||||
Reference in New Issue
Block a user