Files
ccs/src/commands/update-command.ts
T

377 lines
11 KiB
TypeScript

/**
* Update Command Handler
*
* Handles `ccs update` command - checks for updates and installs latest version.
* Supports both npm and direct installation methods.
*/
import { spawn } from 'child_process';
import { initUI, header, ok, fail, warn, info, color } from '../utils/ui';
import { detectInstallationMethod, detectPackageManager } from '../utils/package-manager-detector';
import { compareVersionsWithPrerelease } from '../utils/update-checker';
import { getVersion } from '../utils/version';
/**
* Options for the update command
*/
export interface UpdateOptions {
force?: boolean;
beta?: boolean;
}
// Version (from centralized utility)
const CCS_VERSION = getVersion();
/**
* Handle the update command
* Checks for updates and installs the latest version
*/
export async function handleUpdateCommand(options: UpdateOptions = {}): Promise<void> {
await initUI();
const { force = false, beta = false } = options;
const targetTag = beta ? 'dev' : 'latest';
console.log('');
console.log(header('Checking for updates...'));
console.log('');
const installMethod = detectInstallationMethod();
const isNpmInstall = installMethod === 'npm';
// Force reinstall - skip update check
if (force) {
console.log(info(`Force reinstall from @${targetTag} channel...`));
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, targetTag);
return;
}
if (updateResult.status === 'no_update') {
handleNoUpdate(updateResult.reason);
return;
}
// Update available
console.log(warn(`Update available: ${updateResult.current} -> ${updateResult.latest}`));
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(
warn(
'WARNING: Downgrading from ' +
(updateResult.current || 'unknown') +
' to ' +
(updateResult.latest || 'unknown')
)
);
console.log(warn('Dev channel may be behind stable.'));
console.log('');
}
// Show beta warning
if (beta) {
console.log(warn('Installing from @dev channel (unstable)'));
console.log(warn('Not recommended for production use'));
console.log(info('Use `ccs update` (without --beta) to return to stable'));
console.log('');
}
if (isNpmInstall) {
await performNpmUpdate(targetTag);
} else {
await performDirectUpdate();
}
}
/**
* Handle failed update check
*/
function handleCheckFailed(
message: string,
isNpmInstall: boolean,
targetTag: string = 'latest'
): void {
console.log(fail(message));
console.log('');
console.log(warn('Possible causes:'));
console.log(' - Network connection issues');
console.log(' - Firewall blocking requests');
console.log(' - GitHub/npm API temporarily unavailable');
console.log('');
console.log('Try again later or update manually:');
if (isNpmInstall) {
const packageManager = detectPackageManager();
let manualCommand: string;
switch (packageManager) {
case 'npm':
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
break;
case 'yarn':
manualCommand = `yarn global add @kaitranntt/ccs@${targetTag}`;
break;
case 'pnpm':
manualCommand = `pnpm add -g @kaitranntt/ccs@${targetTag}`;
break;
case 'bun':
manualCommand = `bun add -g @kaitranntt/ccs@${targetTag}`;
break;
default:
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
}
console.log(color(` ${manualCommand}`, 'command'));
} else {
const isWindows = process.platform === 'win32';
if (isWindows) {
console.log(color(' irm ccs.kaitran.ca/install | iex', 'command'));
} else {
console.log(color(' curl -fsSL ccs.kaitran.ca/install | bash', 'command'));
}
}
console.log('');
process.exit(1);
}
/**
* Handle no update available
*/
function handleNoUpdate(reason: string | undefined): void {
const version = getVersion();
let message = `You are already on the latest version (${version})`;
switch (reason) {
case 'dismissed':
message = `Update dismissed. You are on version ${version}`;
console.log(warn(message));
break;
case 'cached':
message = `No updates available (cached result). You are on version ${version}`;
console.log(info(message));
break;
default:
console.log(ok(message));
}
console.log('');
process.exit(0);
}
/**
* Perform update via npm/yarn/pnpm/bun
*/
async function performNpmUpdate(
targetTag: string = 'latest',
isReinstall: boolean = false
): Promise<void> {
const packageManager = detectPackageManager();
let updateCommand: string;
let updateArgs: string[];
let cacheCommand: string | null;
let cacheArgs: string[] | null;
switch (packageManager) {
case 'npm':
updateCommand = 'npm';
updateArgs = ['install', '-g', `@kaitranntt/ccs@${targetTag}`];
cacheCommand = 'npm';
cacheArgs = ['cache', 'clean', '--force'];
break;
case 'yarn':
updateCommand = 'yarn';
updateArgs = ['global', 'add', `@kaitranntt/ccs@${targetTag}`];
cacheCommand = 'yarn';
cacheArgs = ['cache', 'clean'];
break;
case 'pnpm':
updateCommand = 'pnpm';
updateArgs = ['add', '-g', `@kaitranntt/ccs@${targetTag}`];
cacheCommand = 'pnpm';
cacheArgs = ['store', 'prune'];
break;
case 'bun':
updateCommand = 'bun';
updateArgs = ['add', '-g', `@kaitranntt/ccs@${targetTag}`];
cacheCommand = null;
cacheArgs = null;
break;
default:
updateCommand = 'npm';
updateArgs = ['install', '-g', `@kaitranntt/ccs@${targetTag}`];
cacheCommand = 'npm';
cacheArgs = ['cache', 'clean', '--force'];
}
console.log(info(`${isReinstall ? 'Reinstalling' : 'Updating'} via ${packageManager}...`));
console.log('');
const isWindows = process.platform === 'win32';
const performUpdate = (): void => {
// On Windows, use shell with full command string to avoid deprecation warning
const child = isWindows
? spawn(`${updateCommand} ${updateArgs.join(' ')}`, [], { stdio: 'inherit', shell: true })
: spawn(updateCommand, updateArgs, { stdio: 'inherit' });
child.on('exit', (code) => {
if (code === 0) {
console.log('');
console.log(ok(`${isReinstall ? 'Reinstall' : 'Update'} successful!`));
console.log('');
console.log(`Run ${color('ccs --version', 'command')} to verify`);
console.log('');
} else {
console.log('');
console.log(fail(`${isReinstall ? 'Reinstall' : 'Update'} failed`));
console.log('');
console.log('Try manually:');
console.log(color(` ${updateCommand} ${updateArgs.join(' ')}`, 'command'));
console.log('');
}
process.exit(code || 0);
});
child.on('error', () => {
console.log('');
console.log(fail(`Failed to run ${packageManager} ${isReinstall ? 'reinstall' : 'update'}`));
console.log('');
console.log('Try manually:');
console.log(color(` ${updateCommand} ${updateArgs.join(' ')}`, 'command'));
console.log('');
process.exit(1);
});
};
if (cacheCommand && cacheArgs) {
console.log(info('Clearing package cache...'));
// On Windows, use shell with full command string to avoid deprecation warning
const cacheChild = isWindows
? spawn(`${cacheCommand} ${cacheArgs.join(' ')}`, [], { stdio: 'inherit', shell: true })
: spawn(cacheCommand, cacheArgs, { stdio: 'inherit' });
cacheChild.on('exit', (code) => {
if (code !== 0) {
console.log(warn('Cache clearing failed, proceeding anyway...'));
}
performUpdate();
});
cacheChild.on('error', () => {
console.log(warn('Cache clearing failed, proceeding anyway...'));
performUpdate();
});
} else {
performUpdate();
}
}
/**
* Handle direct install beta not supported error
*/
function handleDirectBetaNotSupported(): void {
console.log(fail('--beta flag requires npm installation'));
console.log('');
console.log('Current installation method: direct installer');
console.log('To use beta releases, install via npm:');
console.log('');
console.log(color(' npm install -g @kaitranntt/ccs', 'command'));
console.log(color(' ccs update --beta', 'command'));
console.log('');
console.log('Or continue using stable releases via direct installer.');
console.log('');
process.exit(1);
}
/**
* Perform update via direct installer (curl/irm)
*/
async function performDirectUpdate(): Promise<void> {
console.log(info('Updating via installer...'));
console.log('');
const isWindows = process.platform === 'win32';
let command: string;
let args: string[];
if (isWindows) {
command = 'powershell.exe';
args = [
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-Command',
'irm ccs.kaitran.ca/install | iex',
];
} else {
command = '/bin/bash';
args = ['-c', 'curl -fsSL ccs.kaitran.ca/install | bash'];
}
const child = spawn(command, args, {
stdio: 'inherit',
});
child.on('exit', (code) => {
if (code === 0) {
console.log('');
console.log(ok('Update successful!'));
console.log('');
console.log(`Run ${color('ccs --version', 'command')} to verify`);
console.log('');
} else {
console.log('');
console.log(fail('Update failed'));
console.log('');
console.log('Try manually:');
if (isWindows) {
console.log(color(' irm ccs.kaitran.ca/install | iex', 'command'));
} else {
console.log(color(' curl -fsSL ccs.kaitran.ca/install | bash', 'command'));
}
console.log('');
}
process.exit(code || 0);
});
child.on('error', () => {
console.log('');
console.log(fail('Failed to run installer'));
console.log('');
console.log('Try manually:');
if (isWindows) {
console.log(color(' irm ccs.kaitran.ca/install | iex', 'command'));
} else {
console.log(color(' curl -fsSL ccs.kaitran.ca/install | bash', 'command'));
}
console.log('');
process.exit(1);
});
}