mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 00:17:47 +00:00
feat(ui): enhance doctor and error manager with new ui layer
- doctor.ts: add styled header box, semantic status indicators, formatted summary table, grouped check sections - error-manager.ts: add boxed error messages, styled solution sections, semantic color coding for all error types - ccs.ts, shell-executor.ts: await async error methods Phase 4 of CLI UI Enhancement plan complete.
This commit is contained in:
+2
-2
@@ -284,7 +284,7 @@ async function main(): Promise<void> {
|
|||||||
// Detect Claude CLI first (needed for all paths)
|
// Detect Claude CLI first (needed for all paths)
|
||||||
const claudeCli = detectClaudeCli();
|
const claudeCli = detectClaudeCli();
|
||||||
if (!claudeCli) {
|
if (!claudeCli) {
|
||||||
ErrorManager.showClaudeNotFound();
|
await ErrorManager.showClaudeNotFound();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@ async function main(): Promise<void> {
|
|||||||
// Check if this is a profile not found error with suggestions
|
// Check if this is a profile not found error with suggestions
|
||||||
if (err.profileName && err.availableProfiles !== undefined) {
|
if (err.profileName && err.availableProfiles !== undefined) {
|
||||||
const allProfiles = err.availableProfiles.split('\n');
|
const allProfiles = err.availableProfiles.split('\n');
|
||||||
ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions);
|
await ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions);
|
||||||
} else {
|
} else {
|
||||||
console.error(`[X] ${err.message}`);
|
console.error(`[X] ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+141
-162
@@ -6,8 +6,8 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { colored } from '../utils/helpers';
|
|
||||||
import { detectClaudeCli } from '../utils/claude-detector';
|
import { detectClaudeCli } from '../utils/claude-detector';
|
||||||
|
import { initUI, header, box, table, color, ok, fail, warn, info } from '../utils/ui';
|
||||||
import packageJson from '../../package.json';
|
import packageJson from '../../package.json';
|
||||||
import {
|
import {
|
||||||
isCLIProxyInstalled,
|
isCLIProxyInstalled,
|
||||||
@@ -50,9 +50,6 @@ try {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import cli-table3
|
|
||||||
const Table = require('cli-table3');
|
|
||||||
|
|
||||||
interface HealthCheckDetails {
|
interface HealthCheckDetails {
|
||||||
status: 'OK' | 'ERROR' | 'WARN';
|
status: 'OK' | 'ERROR' | 'WARN';
|
||||||
info: string;
|
info: string;
|
||||||
@@ -133,40 +130,43 @@ class Doctor {
|
|||||||
* Run all health checks
|
* Run all health checks
|
||||||
*/
|
*/
|
||||||
async runAllChecks(): Promise<HealthCheck> {
|
async runAllChecks(): Promise<HealthCheck> {
|
||||||
console.log(colored('Running CCS Health Check...', 'cyan'));
|
await initUI();
|
||||||
|
|
||||||
|
// Hero header box
|
||||||
|
console.log(box(`CCS Health Check v${this.ccsVersion}`, { borderStyle: 'round', padding: 0 }));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Store CCS version in details
|
// Store CCS version in details
|
||||||
this.results.details['CCS Version'] = { status: 'OK', info: `v${this.ccsVersion}` };
|
this.results.details['CCS Version'] = { status: 'OK', info: `v${this.ccsVersion}` };
|
||||||
|
|
||||||
// Group 1: System
|
// Group 1: System
|
||||||
console.log(colored('System:', 'bold'));
|
console.log(header('SYSTEM'));
|
||||||
await this.checkClaudeCli();
|
await this.checkClaudeCli();
|
||||||
this.checkCcsDirectory();
|
this.checkCcsDirectory();
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Group 2: Configuration
|
// Group 2: Configuration
|
||||||
console.log(colored('Configuration:', 'bold'));
|
console.log(header('CONFIGURATION'));
|
||||||
this.checkConfigFiles();
|
this.checkConfigFiles();
|
||||||
this.checkClaudeSettings();
|
this.checkClaudeSettings();
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Group 3: Profiles & Delegation
|
// Group 3: Profiles & Delegation
|
||||||
console.log(colored('Profiles & Delegation:', 'bold'));
|
console.log(header('PROFILES & DELEGATION'));
|
||||||
this.checkProfiles();
|
this.checkProfiles();
|
||||||
this.checkInstances();
|
this.checkInstances();
|
||||||
this.checkDelegation();
|
this.checkDelegation();
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Group 4: System Health
|
// Group 4: System Health
|
||||||
console.log(colored('System Health:', 'bold'));
|
console.log(header('SYSTEM HEALTH'));
|
||||||
this.checkPermissions();
|
this.checkPermissions();
|
||||||
this.checkCcsSymlinks();
|
this.checkCcsSymlinks();
|
||||||
this.checkSettingsSymlinks();
|
this.checkSettingsSymlinks();
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Group 5: CLIProxy (OAuth profiles)
|
// Group 5: CLIProxy (OAuth profiles)
|
||||||
console.log(colored('CLIProxy (OAuth Profiles):', 'bold'));
|
console.log(header('CLIPROXY (OAUTH PROFILES)'));
|
||||||
await this.checkCLIProxy();
|
await this.checkCLIProxy();
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
@@ -183,7 +183,8 @@ class Doctor {
|
|||||||
const claudeCli = detectClaudeCli();
|
const claudeCli = detectClaudeCli();
|
||||||
|
|
||||||
if (!claudeCli) {
|
if (!claudeCli) {
|
||||||
spinner.fail(` ${'Claude CLI'.padEnd(26)}${colored('[X]', 'red')} Not found in PATH`);
|
spinner.fail();
|
||||||
|
console.log(` ${fail('Claude CLI'.padEnd(22))} Not found in PATH`);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Claude CLI',
|
'Claude CLI',
|
||||||
'error',
|
'error',
|
||||||
@@ -218,17 +219,15 @@ class Doctor {
|
|||||||
const versionMatch = result.match(/(\d+\.\d+\.\d+)/);
|
const versionMatch = result.match(/(\d+\.\d+\.\d+)/);
|
||||||
const version = versionMatch ? versionMatch[1] : 'unknown';
|
const version = versionMatch ? versionMatch[1] : 'unknown';
|
||||||
|
|
||||||
spinner.succeed(
|
spinner.succeed();
|
||||||
` ${'Claude CLI'.padEnd(26)}${colored('[OK]', 'green')} ${claudeCli} (v${version})`
|
console.log(` ${ok('Claude CLI'.padEnd(22))} v${version} (${claudeCli})`);
|
||||||
);
|
|
||||||
this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`, undefined, {
|
this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`, undefined, {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
info: `v${version} (${claudeCli})`,
|
info: `v${version} (${claudeCli})`,
|
||||||
});
|
});
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
spinner.fail(
|
spinner.fail();
|
||||||
` ${'Claude CLI'.padEnd(26)}${colored('[X]', 'red')} Not found or not working`
|
console.log(` ${fail('Claude CLI'.padEnd(22))} Not found or not working`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Claude CLI',
|
'Claude CLI',
|
||||||
'error',
|
'error',
|
||||||
@@ -246,13 +245,15 @@ class Doctor {
|
|||||||
const spinner = ora('Checking ~/.ccs/ directory').start();
|
const spinner = ora('Checking ~/.ccs/ directory').start();
|
||||||
|
|
||||||
if (fs.existsSync(this.ccsDir)) {
|
if (fs.existsSync(this.ccsDir)) {
|
||||||
spinner.succeed(` ${'CCS Directory'.padEnd(26)}${colored('[OK]', 'green')} ~/.ccs/`);
|
spinner.succeed();
|
||||||
|
console.log(` ${ok('CCS Directory'.padEnd(22))} ~/.ccs/`);
|
||||||
this.results.addCheck('CCS Directory', 'success', undefined, undefined, {
|
this.results.addCheck('CCS Directory', 'success', undefined, undefined, {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
info: '~/.ccs/',
|
info: '~/.ccs/',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
spinner.fail(` ${'CCS Directory'.padEnd(26)}${colored('[X]', 'red')} Not found`);
|
spinner.fail();
|
||||||
|
console.log(` ${fail('CCS Directory'.padEnd(22))} Not found`);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'CCS Directory',
|
'CCS Directory',
|
||||||
'error',
|
'error',
|
||||||
@@ -289,7 +290,8 @@ class Doctor {
|
|||||||
const spinner = ora(`Checking ${file.name}`).start();
|
const spinner = ora(`Checking ${file.name}`).start();
|
||||||
|
|
||||||
if (!fs.existsSync(file.path)) {
|
if (!fs.existsSync(file.path)) {
|
||||||
spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Not found`);
|
spinner.fail();
|
||||||
|
console.log(` ${fail(file.name.padEnd(22))} Not found`);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
file.name,
|
file.name,
|
||||||
'error',
|
'error',
|
||||||
@@ -306,7 +308,7 @@ class Doctor {
|
|||||||
JSON.parse(content);
|
JSON.parse(content);
|
||||||
|
|
||||||
// Extract useful info based on file type
|
// Extract useful info based on file type
|
||||||
let info = 'Valid';
|
let fileInfo = 'Valid';
|
||||||
let status: 'OK' | 'WARN' = 'OK';
|
let status: 'OK' | 'WARN' = 'OK';
|
||||||
|
|
||||||
if (file.profile) {
|
if (file.profile) {
|
||||||
@@ -314,23 +316,23 @@ class Doctor {
|
|||||||
const validation = DelegationValidator.validate(file.profile);
|
const validation = DelegationValidator.validate(file.profile);
|
||||||
|
|
||||||
if (validation.valid) {
|
if (validation.valid) {
|
||||||
info = 'Key configured';
|
fileInfo = 'Key configured';
|
||||||
status = 'OK';
|
status = 'OK';
|
||||||
} else if (validation.error && validation.error.includes('placeholder')) {
|
} else if (validation.error && validation.error.includes('placeholder')) {
|
||||||
info = 'Placeholder key (not configured)';
|
fileInfo = 'Placeholder key';
|
||||||
status = 'WARN';
|
status = 'WARN';
|
||||||
} else {
|
} else {
|
||||||
info = 'Valid JSON';
|
fileInfo = 'Valid JSON';
|
||||||
status = 'OK';
|
status = 'OK';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusIcon = status === 'OK' ? colored('[OK]', 'green') : colored('[!]', 'yellow');
|
|
||||||
|
|
||||||
if (status === 'WARN') {
|
if (status === 'WARN') {
|
||||||
spinner.warn(` ${file.name.padEnd(26)}${statusIcon} ${info}`);
|
spinner.warn();
|
||||||
|
console.log(` ${warn(file.name.padEnd(22))} ${fileInfo}`);
|
||||||
} else {
|
} else {
|
||||||
spinner.succeed(` ${file.name.padEnd(26)}${statusIcon} ${info}`);
|
spinner.succeed();
|
||||||
|
console.log(` ${ok(file.name.padEnd(22))} ${fileInfo}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
@@ -340,11 +342,12 @@ class Doctor {
|
|||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
status: status,
|
status: status,
|
||||||
info: info,
|
info: fileInfo,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Invalid JSON`);
|
spinner.fail();
|
||||||
|
console.log(` ${fail(file.name.padEnd(22))} Invalid JSON`);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
file.name,
|
file.name,
|
||||||
'error',
|
'error',
|
||||||
@@ -362,11 +365,11 @@ class Doctor {
|
|||||||
private checkClaudeSettings(): void {
|
private checkClaudeSettings(): void {
|
||||||
const spinner = ora('Checking ~/.claude/settings.json').start();
|
const spinner = ora('Checking ~/.claude/settings.json').start();
|
||||||
const settingsPath = path.join(this.claudeDir, 'settings.json');
|
const settingsPath = path.join(this.claudeDir, 'settings.json');
|
||||||
|
const settingsName = '~/.claude/settings.json';
|
||||||
|
|
||||||
if (!fs.existsSync(settingsPath)) {
|
if (!fs.existsSync(settingsPath)) {
|
||||||
spinner.warn(
|
spinner.warn();
|
||||||
` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Not found`
|
console.log(` ${warn(settingsName.padEnd(22))} Not found`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Claude Settings',
|
'Claude Settings',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -380,12 +383,12 @@ class Doctor {
|
|||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(settingsPath, 'utf8');
|
const content = fs.readFileSync(settingsPath, 'utf8');
|
||||||
JSON.parse(content);
|
JSON.parse(content);
|
||||||
spinner.succeed(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[OK]', 'green')}`);
|
spinner.succeed();
|
||||||
|
console.log(` ${ok(settingsName.padEnd(22))} Valid`);
|
||||||
this.results.addCheck('Claude Settings', 'success');
|
this.results.addCheck('Claude Settings', 'success');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
spinner.warn(
|
spinner.warn();
|
||||||
` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Invalid JSON`
|
console.log(` ${warn(settingsName.padEnd(22))} Invalid JSON`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Claude Settings',
|
'Claude Settings',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -403,7 +406,8 @@ class Doctor {
|
|||||||
const configPath = path.join(this.ccsDir, 'config.json');
|
const configPath = path.join(this.ccsDir, 'config.json');
|
||||||
|
|
||||||
if (!fs.existsSync(configPath)) {
|
if (!fs.existsSync(configPath)) {
|
||||||
spinner.info(` ${'Profiles'.padEnd(26)}${colored('[SKIP]', 'cyan')} config.json not found`);
|
spinner.info();
|
||||||
|
console.log(` ${info('Profiles'.padEnd(22))} config.json not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,7 +415,8 @@ class Doctor {
|
|||||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
|
||||||
if (!config.profiles || typeof config.profiles !== 'object') {
|
if (!config.profiles || typeof config.profiles !== 'object') {
|
||||||
spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} Missing profiles object`);
|
spinner.fail();
|
||||||
|
console.log(` ${fail('Profiles'.padEnd(22))} Missing profiles object`);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Profiles',
|
'Profiles',
|
||||||
'error',
|
'error',
|
||||||
@@ -425,9 +430,8 @@ class Doctor {
|
|||||||
const profileCount = Object.keys(config.profiles).length;
|
const profileCount = Object.keys(config.profiles).length;
|
||||||
const profileNames = Object.keys(config.profiles).join(', ');
|
const profileNames = Object.keys(config.profiles).join(', ');
|
||||||
|
|
||||||
spinner.succeed(
|
spinner.succeed();
|
||||||
` ${'Profiles'.padEnd(26)}${colored('[OK]', 'green')} ${profileCount} configured (${profileNames})`
|
console.log(` ${ok('Profiles'.padEnd(22))} ${profileCount} configured (${profileNames})`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Profiles',
|
'Profiles',
|
||||||
'success',
|
'success',
|
||||||
@@ -439,7 +443,8 @@ class Doctor {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} ${(e as Error).message}`);
|
spinner.fail();
|
||||||
|
console.log(` ${fail('Profiles'.padEnd(22))} ${(e as Error).message}`);
|
||||||
this.results.addCheck('Profiles', 'error', (e as Error).message, undefined, {
|
this.results.addCheck('Profiles', 'error', (e as Error).message, undefined, {
|
||||||
status: 'ERROR',
|
status: 'ERROR',
|
||||||
info: (e as Error).message,
|
info: (e as Error).message,
|
||||||
@@ -455,7 +460,8 @@ class Doctor {
|
|||||||
const instancesDir = path.join(this.ccsDir, 'instances');
|
const instancesDir = path.join(this.ccsDir, 'instances');
|
||||||
|
|
||||||
if (!fs.existsSync(instancesDir)) {
|
if (!fs.existsSync(instancesDir)) {
|
||||||
spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`);
|
spinner.info();
|
||||||
|
console.log(` ${info('Instances'.padEnd(22))} No account profiles`);
|
||||||
this.results.addCheck('Instances', 'success', 'No account profiles configured');
|
this.results.addCheck('Instances', 'success', 'No account profiles configured');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -465,14 +471,14 @@ class Doctor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (instances.length === 0) {
|
if (instances.length === 0) {
|
||||||
spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`);
|
spinner.info();
|
||||||
|
console.log(` ${info('Instances'.padEnd(22))} No account profiles`);
|
||||||
this.results.addCheck('Instances', 'success', 'No account profiles');
|
this.results.addCheck('Instances', 'success', 'No account profiles');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
spinner.succeed(
|
spinner.succeed();
|
||||||
` ${'Instances'.padEnd(26)}${colored('[OK]', 'green')} ${instances.length} account profiles`
|
console.log(` ${ok('Instances'.padEnd(22))} ${instances.length} account profiles`);
|
||||||
);
|
|
||||||
this.results.addCheck('Instances', 'success', `${instances.length} account profiles`);
|
this.results.addCheck('Instances', 'success', `${instances.length} account profiles`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +494,8 @@ class Doctor {
|
|||||||
const hasContinueCommand = fs.existsSync(path.join(ccsClaudeCommandsDir, 'ccs', 'continue.md'));
|
const hasContinueCommand = fs.existsSync(path.join(ccsClaudeCommandsDir, 'ccs', 'continue.md'));
|
||||||
|
|
||||||
if (!hasCcsCommand || !hasContinueCommand) {
|
if (!hasCcsCommand || !hasContinueCommand) {
|
||||||
spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} Not installed`);
|
spinner.warn();
|
||||||
|
console.log(` ${warn('Delegation'.padEnd(22))} Not installed`);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Delegation',
|
'Delegation',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -511,7 +518,8 @@ class Doctor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (readyProfiles.length === 0) {
|
if (readyProfiles.length === 0) {
|
||||||
spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} No profiles ready`);
|
spinner.warn();
|
||||||
|
console.log(` ${warn('Delegation'.padEnd(22))} No profiles ready`);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Delegation',
|
'Delegation',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -522,8 +530,9 @@ class Doctor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
spinner.succeed(
|
spinner.succeed();
|
||||||
` ${'Delegation'.padEnd(26)}${colored('[OK]', 'green')} ${readyProfiles.length} profiles ready (${readyProfiles.join(', ')})`
|
console.log(
|
||||||
|
` ${ok('Delegation'.padEnd(22))} ${readyProfiles.length} profiles ready (${readyProfiles.join(', ')})`
|
||||||
);
|
);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Delegation',
|
'Delegation',
|
||||||
@@ -544,17 +553,15 @@ class Doctor {
|
|||||||
try {
|
try {
|
||||||
fs.writeFileSync(testFile, 'test', 'utf8');
|
fs.writeFileSync(testFile, 'test', 'utf8');
|
||||||
fs.unlinkSync(testFile);
|
fs.unlinkSync(testFile);
|
||||||
spinner.succeed(
|
spinner.succeed();
|
||||||
` ${'Permissions'.padEnd(26)}${colored('[OK]', 'green')} Write access verified`
|
console.log(` ${ok('Permissions'.padEnd(22))} Write access verified`);
|
||||||
);
|
|
||||||
this.results.addCheck('Permissions', 'success', undefined, undefined, {
|
this.results.addCheck('Permissions', 'success', undefined, undefined, {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
info: 'Write access verified',
|
info: 'Write access verified',
|
||||||
});
|
});
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
spinner.fail(
|
spinner.fail();
|
||||||
` ${'Permissions'.padEnd(26)}${colored('[X]', 'red')} Cannot write to ~/.ccs/`
|
console.log(` ${fail('Permissions'.padEnd(22))} Cannot write to ~/.ccs/`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Permissions',
|
'Permissions',
|
||||||
'error',
|
'error',
|
||||||
@@ -578,9 +585,8 @@ class Doctor {
|
|||||||
|
|
||||||
if (health.healthy) {
|
if (health.healthy) {
|
||||||
const itemCount = manager.ccsItems.length;
|
const itemCount = manager.ccsItems.length;
|
||||||
spinner.succeed(
|
spinner.succeed();
|
||||||
` ${'CCS Symlinks'.padEnd(26)}${colored('[OK]', 'green')} ${itemCount}/${itemCount} items linked`
|
console.log(` ${ok('CCS Symlinks'.padEnd(22))} ${itemCount}/${itemCount} items linked`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'CCS Symlinks',
|
'CCS Symlinks',
|
||||||
'success',
|
'success',
|
||||||
@@ -592,9 +598,8 @@ class Doctor {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
spinner.warn(
|
spinner.warn();
|
||||||
` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} ${health.issues.length} issues found`
|
console.log(` ${warn('CCS Symlinks'.padEnd(22))} ${health.issues.length} issues found`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'CCS Symlinks',
|
'CCS Symlinks',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -604,7 +609,8 @@ class Doctor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
spinner.warn(` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} Could not check`);
|
spinner.warn();
|
||||||
|
console.log(` ${warn('CCS Symlinks'.padEnd(22))} Could not check`);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'CCS Symlinks',
|
'CCS Symlinks',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -620,6 +626,7 @@ class Doctor {
|
|||||||
*/
|
*/
|
||||||
private checkSettingsSymlinks(): void {
|
private checkSettingsSymlinks(): void {
|
||||||
const spinner = ora('Checking settings.json symlinks').start();
|
const spinner = ora('Checking settings.json symlinks').start();
|
||||||
|
const settingsLabel = 'settings.json';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sharedDir = path.join(this.homedir, '.ccs', 'shared');
|
const sharedDir = path.join(this.homedir, '.ccs', 'shared');
|
||||||
@@ -628,9 +635,8 @@ class Doctor {
|
|||||||
|
|
||||||
// Check shared settings exists and points to ~/.claude/
|
// Check shared settings exists and points to ~/.claude/
|
||||||
if (!fs.existsSync(sharedSettings)) {
|
if (!fs.existsSync(sharedSettings)) {
|
||||||
spinner.warn(
|
spinner.warn();
|
||||||
` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Not found`
|
console.log(` ${warn(settingsLabel.padEnd(22))} Not found (shared)`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Settings Symlinks',
|
'Settings Symlinks',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -642,9 +648,8 @@ class Doctor {
|
|||||||
|
|
||||||
const sharedStats = fs.lstatSync(sharedSettings);
|
const sharedStats = fs.lstatSync(sharedSettings);
|
||||||
if (!sharedStats.isSymbolicLink()) {
|
if (!sharedStats.isSymbolicLink()) {
|
||||||
spinner.warn(
|
spinner.warn();
|
||||||
` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Not a symlink`
|
console.log(` ${warn(settingsLabel.padEnd(22))} Not a symlink (shared)`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Settings Symlinks',
|
'Settings Symlinks',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -658,9 +663,8 @@ class Doctor {
|
|||||||
const resolvedShared = path.resolve(path.dirname(sharedSettings), sharedTarget);
|
const resolvedShared = path.resolve(path.dirname(sharedSettings), sharedTarget);
|
||||||
|
|
||||||
if (resolvedShared !== claudeSettings) {
|
if (resolvedShared !== claudeSettings) {
|
||||||
spinner.warn(
|
spinner.warn();
|
||||||
` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Wrong target`
|
console.log(` ${warn(settingsLabel.padEnd(22))} Wrong target (shared)`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Settings Symlinks',
|
'Settings Symlinks',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -673,9 +677,8 @@ class Doctor {
|
|||||||
// Check each instance
|
// Check each instance
|
||||||
const instancesDir = path.join(this.ccsDir, 'instances');
|
const instancesDir = path.join(this.ccsDir, 'instances');
|
||||||
if (!fs.existsSync(instancesDir)) {
|
if (!fs.existsSync(instancesDir)) {
|
||||||
spinner.succeed(
|
spinner.succeed();
|
||||||
` ${'settings.json'.padEnd(26)}${colored('[OK]', 'green')} Shared symlink valid`
|
console.log(` ${ok(settingsLabel.padEnd(22))} Shared symlink valid`);
|
||||||
);
|
|
||||||
this.results.addCheck('Settings Symlinks', 'success', 'Shared symlink valid', undefined, {
|
this.results.addCheck('Settings Symlinks', 'success', 'Shared symlink valid', undefined, {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
info: 'Shared symlink valid',
|
info: 'Shared symlink valid',
|
||||||
@@ -716,9 +719,8 @@ class Doctor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (broken > 0) {
|
if (broken > 0) {
|
||||||
spinner.warn(
|
spinner.warn();
|
||||||
` ${'settings.json'.padEnd(26)}${colored('[!]', 'yellow')} ${broken} broken instance(s)`
|
console.log(` ${warn(settingsLabel.padEnd(22))} ${broken} broken instance(s)`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Settings Symlinks',
|
'Settings Symlinks',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -727,9 +729,8 @@ class Doctor {
|
|||||||
{ status: 'WARN', info: `${broken} broken instance(s)` }
|
{ status: 'WARN', info: `${broken} broken instance(s)` }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
spinner.succeed(
|
spinner.succeed();
|
||||||
` ${'settings.json'.padEnd(26)}${colored('[OK]', 'green')} ${instances.length} instance(s) valid`
|
console.log(` ${ok(settingsLabel.padEnd(22))} ${instances.length} instance(s) valid`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Settings Symlinks',
|
'Settings Symlinks',
|
||||||
'success',
|
'success',
|
||||||
@@ -742,7 +743,8 @@ class Doctor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
spinner.warn(` ${'settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Check failed`);
|
spinner.warn();
|
||||||
|
console.log(` ${warn(settingsLabel.padEnd(22))} Check failed`);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'Settings Symlinks',
|
'Settings Symlinks',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -762,16 +764,16 @@ class Doctor {
|
|||||||
|
|
||||||
if (isCLIProxyInstalled()) {
|
if (isCLIProxyInstalled()) {
|
||||||
const binaryPath = getCLIProxyPath();
|
const binaryPath = getCLIProxyPath();
|
||||||
binarySpinner.succeed(
|
binarySpinner.succeed();
|
||||||
` ${'CLIProxy Binary'.padEnd(26)}${colored('[OK]', 'green')} v${CLIPROXY_VERSION}`
|
console.log(` ${ok('CLIProxy Binary'.padEnd(22))} v${CLIPROXY_VERSION}`);
|
||||||
);
|
|
||||||
this.results.addCheck('CLIProxy Binary', 'success', undefined, undefined, {
|
this.results.addCheck('CLIProxy Binary', 'success', undefined, undefined, {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
info: `v${CLIPROXY_VERSION} (${binaryPath})`,
|
info: `v${CLIPROXY_VERSION} (${binaryPath})`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
binarySpinner.info(
|
binarySpinner.info();
|
||||||
` ${'CLIProxy Binary'.padEnd(26)}${colored('[i]', 'cyan')} Not installed (downloads on first use)`
|
console.log(
|
||||||
|
` ${info('CLIProxy Binary'.padEnd(22))} Not installed (downloads on first use)`
|
||||||
);
|
);
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'CLIProxy Binary',
|
'CLIProxy Binary',
|
||||||
@@ -787,17 +789,15 @@ class Doctor {
|
|||||||
const configPath = getConfigPath();
|
const configPath = getConfigPath();
|
||||||
|
|
||||||
if (fs.existsSync(configPath)) {
|
if (fs.existsSync(configPath)) {
|
||||||
configSpinner.succeed(
|
configSpinner.succeed();
|
||||||
` ${'CLIProxy Config'.padEnd(26)}${colored('[OK]', 'green')} cliproxy/config.yaml`
|
console.log(` ${ok('CLIProxy Config'.padEnd(22))} cliproxy/config.yaml`);
|
||||||
);
|
|
||||||
this.results.addCheck('CLIProxy Config', 'success', undefined, undefined, {
|
this.results.addCheck('CLIProxy Config', 'success', undefined, undefined, {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
info: 'cliproxy/config.yaml',
|
info: 'cliproxy/config.yaml',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
configSpinner.info(
|
configSpinner.info();
|
||||||
` ${'CLIProxy Config'.padEnd(26)}${colored('[i]', 'cyan')} Not created (generated on first use)`
|
console.log(` ${info('CLIProxy Config'.padEnd(22))} Not created (on first use)`);
|
||||||
);
|
|
||||||
this.results.addCheck('CLIProxy Config', 'success', 'Not created yet', undefined, {
|
this.results.addCheck('CLIProxy Config', 'success', 'Not created yet', undefined, {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
info: 'Generated on first use',
|
info: 'Generated on first use',
|
||||||
@@ -812,17 +812,15 @@ class Doctor {
|
|||||||
|
|
||||||
if (status.authenticated) {
|
if (status.authenticated) {
|
||||||
const lastAuth = status.lastAuth ? ` (${status.lastAuth.toLocaleDateString()})` : '';
|
const lastAuth = status.lastAuth ? ` (${status.lastAuth.toLocaleDateString()})` : '';
|
||||||
authSpinner.succeed(
|
authSpinner.succeed();
|
||||||
` ${`${providerName} Auth`.padEnd(26)}${colored('[OK]', 'green')} Authenticated${lastAuth}`
|
console.log(` ${ok(`${providerName} Auth`.padEnd(22))} Authenticated${lastAuth}`);
|
||||||
);
|
|
||||||
this.results.addCheck(`${providerName} Auth`, 'success', undefined, undefined, {
|
this.results.addCheck(`${providerName} Auth`, 'success', undefined, undefined, {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
info: `Authenticated${lastAuth}`,
|
info: `Authenticated${lastAuth}`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
authSpinner.info(
|
authSpinner.info();
|
||||||
` ${`${providerName} Auth`.padEnd(26)}${colored('[i]', 'cyan')} Not authenticated`
|
console.log(` ${info(`${providerName} Auth`.padEnd(22))} Not authenticated`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
`${providerName} Auth`,
|
`${providerName} Auth`,
|
||||||
'success',
|
'success',
|
||||||
@@ -838,17 +836,15 @@ class Doctor {
|
|||||||
const portAvailable = await isPortAvailable(CLIPROXY_DEFAULT_PORT);
|
const portAvailable = await isPortAvailable(CLIPROXY_DEFAULT_PORT);
|
||||||
|
|
||||||
if (portAvailable) {
|
if (portAvailable) {
|
||||||
portSpinner.succeed(
|
portSpinner.succeed();
|
||||||
` ${'CLIProxy Port'.padEnd(26)}${colored('[OK]', 'green')} ${CLIPROXY_DEFAULT_PORT} available`
|
console.log(` ${ok('CLIProxy Port'.padEnd(22))} ${CLIPROXY_DEFAULT_PORT} available`);
|
||||||
);
|
|
||||||
this.results.addCheck('CLIProxy Port', 'success', undefined, undefined, {
|
this.results.addCheck('CLIProxy Port', 'success', undefined, undefined, {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
info: `Port ${CLIPROXY_DEFAULT_PORT} available`,
|
info: `Port ${CLIPROXY_DEFAULT_PORT} available`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
portSpinner.warn(
|
portSpinner.warn();
|
||||||
` ${'CLIProxy Port'.padEnd(26)}${colored('[!]', 'yellow')} ${CLIPROXY_DEFAULT_PORT} in use`
|
console.log(` ${warn('CLIProxy Port'.padEnd(22))} ${CLIPROXY_DEFAULT_PORT} in use`);
|
||||||
);
|
|
||||||
this.results.addCheck(
|
this.results.addCheck(
|
||||||
'CLIProxy Port',
|
'CLIProxy Port',
|
||||||
'warning',
|
'warning',
|
||||||
@@ -864,68 +860,48 @@ class Doctor {
|
|||||||
*/
|
*/
|
||||||
private showReport(): void {
|
private showReport(): void {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
console.log(header('HEALTH CHECK SUMMARY'));
|
||||||
// Calculate exact table width to match header bars
|
|
||||||
// colWidths: [20, 10, 35] = 65 + borders (4) = 69 total
|
|
||||||
const tableWidth = 69;
|
|
||||||
const headerBar = '═'.repeat(tableWidth);
|
|
||||||
|
|
||||||
console.log(colored(headerBar, 'cyan'));
|
|
||||||
console.log(colored(' Health Check Summary', 'bold'));
|
|
||||||
console.log(colored(headerBar, 'cyan'));
|
|
||||||
|
|
||||||
// Create summary table with detailed information
|
|
||||||
const table = new Table({
|
|
||||||
head: [colored('Component', 'cyan'), colored('Status', 'cyan'), colored('Details', 'cyan')],
|
|
||||||
colWidths: [20, 10, 35],
|
|
||||||
wordWrap: true,
|
|
||||||
chars: {
|
|
||||||
top: '═',
|
|
||||||
'top-mid': '╤',
|
|
||||||
'top-left': '╔',
|
|
||||||
'top-right': '╗',
|
|
||||||
bottom: '═',
|
|
||||||
'bottom-mid': '╧',
|
|
||||||
'bottom-left': '╚',
|
|
||||||
'bottom-right': '╝',
|
|
||||||
left: '║',
|
|
||||||
'left-mid': '╟',
|
|
||||||
mid: '─',
|
|
||||||
'mid-mid': '┼',
|
|
||||||
right: '║',
|
|
||||||
'right-mid': '╢',
|
|
||||||
middle: '│',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Populate table with collected details
|
|
||||||
for (const [component, detail] of Object.entries(this.results.details)) {
|
|
||||||
const statusColor =
|
|
||||||
detail.status === 'OK' ? 'green' : detail.status === 'ERROR' ? 'red' : 'yellow';
|
|
||||||
table.push([component, colored(detail.status, statusColor), detail.info || '']);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(table.toString());
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Show errors and warnings if present
|
// Build summary table rows
|
||||||
|
const rows: string[][] = Object.entries(this.results.details).map(([component, detail]) => {
|
||||||
|
const statusIndicator =
|
||||||
|
detail.status === 'OK'
|
||||||
|
? color('[OK]', 'success')
|
||||||
|
: detail.status === 'ERROR'
|
||||||
|
? color('[X]', 'error')
|
||||||
|
: color('[!]', 'warning');
|
||||||
|
|
||||||
|
return [component, statusIndicator, detail.info || ''];
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
table(rows, {
|
||||||
|
head: ['Component', 'Status', 'Details'],
|
||||||
|
colWidths: [20, 12, 35],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Show errors if present
|
||||||
if (this.results.hasErrors()) {
|
if (this.results.hasErrors()) {
|
||||||
console.log(colored('Errors:', 'red'));
|
console.log(header('ERRORS'));
|
||||||
this.results.errors.forEach((err) => {
|
this.results.errors.forEach((err) => {
|
||||||
console.log(` [X] ${err.name}: ${err.message}`);
|
console.log(` ${fail(err.name)}: ${err.message}`);
|
||||||
if (err.fix) {
|
if (err.fix) {
|
||||||
console.log(` Fix: ${err.fix}`);
|
console.log(` Fix: ${color(err.fix, 'command')}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show warnings if present
|
||||||
if (this.results.hasWarnings()) {
|
if (this.results.hasWarnings()) {
|
||||||
console.log(colored('Warnings:', 'yellow'));
|
console.log(header('WARNINGS'));
|
||||||
this.results.warnings.forEach((warn) => {
|
this.results.warnings.forEach((w) => {
|
||||||
console.log(` [!] ${warn.name}: ${warn.message}`);
|
console.log(` ${warn(w.name)}: ${w.message}`);
|
||||||
if (warn.fix) {
|
if (w.fix) {
|
||||||
console.log(` Fix: ${warn.fix}`);
|
console.log(` Fix: ${color(w.fix, 'command')}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -933,12 +909,15 @@ class Doctor {
|
|||||||
|
|
||||||
// Final status
|
// Final status
|
||||||
if (this.results.isHealthy() && !this.results.hasWarnings()) {
|
if (this.results.isHealthy() && !this.results.hasWarnings()) {
|
||||||
console.log(colored('[OK] All checks passed! Your CCS installation is healthy.', 'green'));
|
console.log(ok('All checks passed! Installation is healthy.'));
|
||||||
} else if (this.results.hasErrors()) {
|
} else if (this.results.hasErrors()) {
|
||||||
console.log(colored('[X] Status: Installation has errors', 'red'));
|
console.log(fail('Installation has errors. Run suggested fixes above.'));
|
||||||
console.log('Run suggested fixes above to resolve issues.');
|
|
||||||
} else {
|
} else {
|
||||||
console.log(colored('[OK] Status: Installation healthy (warnings only)', 'green'));
|
console.log(
|
||||||
|
ok(
|
||||||
|
`Installation healthy (${this.results.warnings.length} warning${this.results.warnings.length !== 1 ? 's' : ''})`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
+127
-93
@@ -1,4 +1,4 @@
|
|||||||
import { colored } from './helpers';
|
import { initUI, header, color, dim, errorBox } from './ui';
|
||||||
import { ERROR_CODES, getErrorDocUrl, ErrorCode } from './error-codes';
|
import { ERROR_CODES, getErrorDocUrl, ErrorCode } from './error-codes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,60 +23,70 @@ export class ErrorManager {
|
|||||||
* Show error code and documentation URL
|
* Show error code and documentation URL
|
||||||
*/
|
*/
|
||||||
static showErrorCode(errorCode: ErrorCode): void {
|
static showErrorCode(errorCode: ErrorCode): void {
|
||||||
console.error(colored(`Error: ${errorCode}`, 'yellow'));
|
console.error(dim(`Error: ${errorCode}`));
|
||||||
console.error(colored(getErrorDocUrl(errorCode), 'yellow'));
|
console.error(dim(getErrorDocUrl(errorCode)));
|
||||||
console.error('');
|
console.error('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show Claude CLI not found error
|
* Show Claude CLI not found error
|
||||||
*/
|
*/
|
||||||
static showClaudeNotFound(): void {
|
static async showClaudeNotFound(): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('[X] Claude CLI not found', 'red'));
|
console.error(
|
||||||
|
errorBox(
|
||||||
|
'Claude CLI not found\n\n' +
|
||||||
|
'CCS requires Claude CLI to be installed\n' +
|
||||||
|
'and available in PATH.',
|
||||||
|
'ERROR'
|
||||||
|
)
|
||||||
|
);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('CCS requires Claude CLI to be installed and available in PATH.');
|
|
||||||
|
console.error(header('SOLUTIONS'));
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('Solutions:', 'yellow'));
|
console.error(' 1. Install Claude CLI');
|
||||||
console.error(' 1. Install Claude CLI:');
|
console.error(` ${color('https://docs.claude.com/install', 'path')}`);
|
||||||
console.error(' https://docs.claude.com/en/docs/claude-code/installation');
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(' 2. Verify installation:');
|
console.error(' 2. Verify installation');
|
||||||
console.error(' command -v claude (Unix)');
|
console.error(` ${color('command -v claude', 'command')} (Unix)`);
|
||||||
console.error(' Get-Command claude (Windows)');
|
console.error(` ${color('Get-Command claude', 'command')} (Windows)`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(' 3. Custom path (if installed elsewhere):');
|
console.error(' 3. Custom path (if installed elsewhere)');
|
||||||
console.error(' export CCS_CLAUDE_PATH="/path/to/claude"');
|
console.error(` ${color('export CCS_CLAUDE_PATH="/path/to/claude"', 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
|
|
||||||
this.showErrorCode(ERROR_CODES.CLAUDE_NOT_FOUND);
|
this.showErrorCode(ERROR_CODES.CLAUDE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show settings file not found error
|
* Show settings file not found error
|
||||||
*/
|
*/
|
||||||
static showSettingsNotFound(settingsPath: string): void {
|
static async showSettingsNotFound(settingsPath: string): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
const isClaudeSettings =
|
const isClaudeSettings =
|
||||||
settingsPath.includes('.claude') && settingsPath.endsWith('settings.json');
|
settingsPath.includes('.claude') && settingsPath.endsWith('settings.json');
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('[X] Settings file not found', 'red'));
|
console.error(errorBox('Settings file not found\n\n' + `File: ${settingsPath}`, 'ERROR'));
|
||||||
console.error('');
|
|
||||||
console.error(`File: ${settingsPath}`);
|
|
||||||
console.error('');
|
console.error('');
|
||||||
|
|
||||||
if (isClaudeSettings) {
|
if (isClaudeSettings) {
|
||||||
console.error('This file is auto-created when you login to Claude CLI.');
|
console.error('This file is auto-created when you login to Claude CLI.');
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('Solutions:', 'yellow'));
|
console.error(header('SOLUTIONS'));
|
||||||
console.error(` echo '{}' > ${settingsPath}`);
|
console.error(` ${color(`echo '{}' > ${settingsPath}`, 'command')}`);
|
||||||
console.error(' claude /login');
|
console.error(` ${color('claude /login', 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('Why: Newer Claude CLI versions require explicit login.');
|
console.error(dim('Why: Newer Claude CLI versions require explicit login.'));
|
||||||
} else {
|
} else {
|
||||||
console.error(colored('Solutions:', 'yellow'));
|
console.error(header('SOLUTIONS'));
|
||||||
console.error(' npm install -g @kaitranntt/ccs --force');
|
console.error(` ${color('npm install -g @kaitranntt/ccs --force', 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('This will recreate missing profile settings.');
|
console.error(dim('This will recreate missing profile settings.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
@@ -86,120 +96,139 @@ export class ErrorManager {
|
|||||||
/**
|
/**
|
||||||
* Show invalid configuration error
|
* Show invalid configuration error
|
||||||
*/
|
*/
|
||||||
static showInvalidConfig(configPath: string, errorDetail: string): void {
|
static async showInvalidConfig(configPath: string, errorDetail: string): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('[X] Configuration invalid', 'red'));
|
console.error(
|
||||||
|
errorBox(
|
||||||
|
'Configuration invalid\n\n' + `File: ${configPath}\n` + `Issue: ${errorDetail}`,
|
||||||
|
'ERROR'
|
||||||
|
)
|
||||||
|
);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(`File: ${configPath}`);
|
|
||||||
console.error(`Issue: ${errorDetail}`);
|
console.error(header('SOLUTIONS'));
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('Solutions:', 'yellow'));
|
console.error(` ${dim('# Backup corrupted file')}`);
|
||||||
console.error(' # Backup corrupted file');
|
console.error(` ${color(`mv ${configPath} ${configPath}.backup`, 'command')}`);
|
||||||
console.error(` mv ${configPath} ${configPath}.backup`);
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(' # Reinstall CCS');
|
console.error(` ${dim('# Reinstall CCS')}`);
|
||||||
console.error(' npm install -g @kaitranntt/ccs --force');
|
console.error(` ${color('npm install -g @kaitranntt/ccs --force', 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('Your profile settings will be preserved.');
|
console.error(dim('Your profile settings will be preserved.'));
|
||||||
console.error('');
|
console.error('');
|
||||||
|
|
||||||
this.showErrorCode(ERROR_CODES.CONFIG_INVALID_JSON);
|
this.showErrorCode(ERROR_CODES.CONFIG_INVALID_JSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show profile not found error
|
* Show profile not found error
|
||||||
*/
|
*/
|
||||||
static showProfileNotFound(
|
static async showProfileNotFound(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
availableProfiles: string[],
|
availableProfiles: string[],
|
||||||
suggestions: string[] = []
|
suggestions: string[] = []
|
||||||
): void {
|
): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored(`[X] Profile '${profileName}' not found`, 'red'));
|
console.error(errorBox(`Profile '${profileName}' not found`, 'ERROR'));
|
||||||
console.error('');
|
console.error('');
|
||||||
|
|
||||||
if (suggestions && suggestions.length > 0) {
|
if (suggestions && suggestions.length > 0) {
|
||||||
console.error(colored('Did you mean:', 'yellow'));
|
console.error(header('DID YOU MEAN'));
|
||||||
suggestions.forEach((s) => console.error(` ${s}`));
|
suggestions.forEach((s) => console.error(` ${color(s, 'command')}`));
|
||||||
console.error('');
|
console.error('');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(colored('Available profiles:', 'cyan'));
|
console.error(header('AVAILABLE PROFILES'));
|
||||||
availableProfiles.forEach((line) => console.error(` ${line}`));
|
availableProfiles.forEach((line) => console.error(` ${color(line, 'info')}`));
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('Solutions:', 'yellow'));
|
|
||||||
console.error(' # Use existing profile');
|
console.error(header('SOLUTIONS'));
|
||||||
console.error(' ccs <profile> "your prompt"');
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(' # Create new account profile');
|
console.error(' Use an existing profile:');
|
||||||
console.error(' ccs auth create <name>');
|
console.error(` ${color('ccs <profile> "your prompt"', 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
|
console.error(' Create a new account profile:');
|
||||||
|
console.error(` ${color('ccs auth create <name>', 'command')}`);
|
||||||
|
console.error('');
|
||||||
|
|
||||||
this.showErrorCode(ERROR_CODES.PROFILE_NOT_FOUND);
|
this.showErrorCode(ERROR_CODES.PROFILE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show permission denied error
|
* Show permission denied error
|
||||||
*/
|
*/
|
||||||
static showPermissionDenied(path: string): void {
|
static async showPermissionDenied(filePath: string): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('[X] Permission denied', 'red'));
|
console.error(errorBox('Permission denied\n\n' + `Cannot write to: ${filePath}`, 'ERROR'));
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(`Cannot write to: ${path}`);
|
|
||||||
|
console.error(header('SOLUTIONS'));
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('Solutions:', 'yellow'));
|
console.error(` ${dim('# Fix ownership')}`);
|
||||||
console.error(' # Fix ownership');
|
console.error(` ${color('sudo chown -R $USER ~/.ccs ~/.claude', 'command')}`);
|
||||||
console.error(' sudo chown -R $USER ~/.ccs ~/.claude');
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(' # Fix permissions');
|
console.error(` ${dim('# Fix permissions')}`);
|
||||||
console.error(' chmod 755 ~/.ccs ~/.claude');
|
console.error(` ${color('chmod 755 ~/.ccs ~/.claude', 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(' # Retry installation');
|
console.error(` ${dim('# Retry installation')}`);
|
||||||
console.error(' npm install -g @kaitranntt/ccs --force');
|
console.error(` ${color('npm install -g @kaitranntt/ccs --force', 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
|
|
||||||
this.showErrorCode(ERROR_CODES.FS_CANNOT_WRITE_FILE);
|
this.showErrorCode(ERROR_CODES.FS_CANNOT_WRITE_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show CLIProxy OAuth timeout error
|
* Show CLIProxy OAuth timeout error
|
||||||
*/
|
*/
|
||||||
static showOAuthTimeout(provider: string): void {
|
static async showOAuthTimeout(provider: string): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
console.error(
|
||||||
console.error(colored('| OAuth Timeout |', 'red'));
|
errorBox('OAuth Timeout\n\n' + 'Authentication did not complete within 2 minutes.', 'ERROR')
|
||||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('Authentication did not complete within 2 minutes.');
|
|
||||||
console.error('');
|
console.error(header('TROUBLESHOOTING'));
|
||||||
console.error(colored('Troubleshooting:', 'yellow'));
|
|
||||||
console.error(' 1. Check if browser opened (popup blocker?)');
|
console.error(' 1. Check if browser opened (popup blocker?)');
|
||||||
console.error(' 2. Complete login in browser, then return here');
|
console.error(' 2. Complete login in browser, then return here');
|
||||||
console.error(' 3. Try different browser');
|
console.error(' 3. Try different browser');
|
||||||
console.error(' 4. Disable browser extensions temporarily');
|
console.error(' 4. Disable browser extensions temporarily');
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('For headless/SSH environments:', 'cyan'));
|
|
||||||
console.error(` ccs ${provider} --auth --headless`);
|
console.error(header('FOR HEADLESS/SSH ENVIRONMENTS'));
|
||||||
|
console.error(` ${color(`ccs ${provider} --auth --headless`, 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('This displays manual authentication steps.');
|
console.error(dim('This displays manual authentication steps.'));
|
||||||
console.error('');
|
console.error('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show CLIProxy port conflict error
|
* Show CLIProxy port conflict error
|
||||||
*/
|
*/
|
||||||
static showPortConflict(port: number): void {
|
static async showPortConflict(port: number): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
console.error(
|
||||||
console.error(colored('| Port Conflict |', 'red'));
|
errorBox('Port Conflict\n\n' + `CLIProxy port ${port} is already in use.`, 'ERROR')
|
||||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(`CLIProxy port ${port} is already in use.`);
|
|
||||||
|
console.error(header('SOLUTIONS'));
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('Solutions:', 'yellow'));
|
|
||||||
console.error(' 1. Find process using port:');
|
console.error(' 1. Find process using port:');
|
||||||
console.error(` lsof -i :${port} (macOS/Linux)`);
|
console.error(` ${color(`lsof -i :${port}`, 'command')} (macOS/Linux)`);
|
||||||
console.error(` netstat -ano | findstr ${port} (Windows)`);
|
console.error(` ${color(`netstat -ano | findstr ${port}`, 'command')} (Windows)`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(' 2. Kill the process:');
|
console.error(' 2. Kill the process:');
|
||||||
console.error(` lsof -ti:${port} | xargs kill -9`);
|
console.error(` ${color(`lsof -ti:${port} | xargs kill -9`, 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(' 3. Wait and retry (process may exit on its own)');
|
console.error(' 3. Wait and retry (process may exit on its own)');
|
||||||
console.error('');
|
console.error('');
|
||||||
@@ -208,38 +237,43 @@ export class ErrorManager {
|
|||||||
/**
|
/**
|
||||||
* Show CLIProxy binary download failure error
|
* Show CLIProxy binary download failure error
|
||||||
*/
|
*/
|
||||||
static showBinaryDownloadFailed(url: string, error: string): void {
|
static async showBinaryDownloadFailed(url: string, error: string): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
console.error(errorBox('Binary Download Failed\n\n' + `Error: ${error}`, 'ERROR'));
|
||||||
console.error(colored('| Binary Download Failed |', 'red'));
|
|
||||||
console.error(colored('+---------------------------------------------------------+', 'red'));
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(`Error: ${error}`);
|
|
||||||
console.error('');
|
console.error(header('TROUBLESHOOTING'));
|
||||||
console.error(colored('Troubleshooting:', 'yellow'));
|
|
||||||
console.error(' 1. Check internet connection');
|
console.error(' 1. Check internet connection');
|
||||||
console.error(' 2. Check firewall/proxy settings');
|
console.error(' 2. Check firewall/proxy settings');
|
||||||
console.error(' 3. Try again in a few minutes');
|
console.error(' 3. Try again in a few minutes');
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('Manual download:', 'cyan'));
|
|
||||||
console.error(` URL: ${url}`);
|
console.error(header('MANUAL DOWNLOAD'));
|
||||||
console.error(' Save to: ~/.ccs/bin/cliproxyapi');
|
console.error(` URL: ${color(url, 'path')}`);
|
||||||
console.error(' chmod +x ~/.ccs/bin/cliproxyapi');
|
console.error(` Save to: ${color('~/.ccs/bin/cliproxyapi', 'path')}`);
|
||||||
|
console.error(` ${color('chmod +x ~/.ccs/bin/cliproxyapi', 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show CLIProxy authentication required error
|
* Show CLIProxy authentication required error
|
||||||
*/
|
*/
|
||||||
static showAuthRequired(provider: string): void {
|
static async showAuthRequired(provider: string): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
|
const displayName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
||||||
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored(`[X] ${provider} authentication required`, 'red'));
|
console.error(errorBox(`${displayName} authentication required`, 'ERROR'));
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(colored('To authenticate:', 'yellow'));
|
|
||||||
console.error(` ccs ${provider} --auth`);
|
console.error(header('TO AUTHENTICATE'));
|
||||||
|
console.error(` ${color(`ccs ${provider} --auth`, 'command')}`);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('This will open a browser for OAuth login.');
|
console.error(dim('This will open a browser for OAuth login.'));
|
||||||
console.error('After login, you can use the profile normally.');
|
console.error(dim('After login, you can use the profile normally.'));
|
||||||
console.error('');
|
console.error('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ export function execClaude(
|
|||||||
else process.exit(code || 0);
|
else process.exit(code || 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', () => {
|
child.on('error', async () => {
|
||||||
ErrorManager.showClaudeNotFound();
|
await ErrorManager.showClaudeNotFound();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user