Files
ccs/bin/auth/profile-detector.js
T
Kai (Tam Nhu) TranandGitHub f40e9647ec feat(cli): comprehensive UX improvements for v3.5.0 (#7)
Implements 6-phase CLI UX improvement plan with comprehensive error handling, 
interactive features, and cross-platform consistency.

### Added
- Shell auto-completion (bash, zsh, PowerShell, Fish)
- Error codes (E101-E901) with documentation URLs
- Fuzzy matching "Did you mean?" suggestions (Levenshtein distance)
- Progress indicators (doctor [n/9] counter, GLMT proxy spinner)
- Interactive confirmation prompts with --yes/-y automation flag
- JSON output format (--json) for auth commands
- Impact display (session count, paths) before destructive operations
- Comprehensive test suite (15 tests, 100% pass rate)
- Complete error documentation in docs/errors/
- Cross-platform `--shell-completion` command

### Changed
- Error boxes: Unicode (╔═╗) → ASCII (===) for compatibility
- JSON output uses CCS version instead of schema version
- Help text includes EXAMPLES section across platforms
- Test suite properly counts test cases (not assertions)

### Fixed
- --yes flag bug (returned false instead of true)
- Help text consistency (added Uninstall section to bash)
- Test pass rate calculation (excludes skipped tests)
- Help section comparison (locale-specific sort)

### Testing
- 13/13 tests passing (2 legitimately skipped)
- Cross-platform verified (Node.js, bash, PowerShell)
- All error codes documented and tested
2025-11-15 01:26:50 -05:00

205 lines
5.5 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
const { findSimilarStrings } = require('../utils/helpers');
/**
* Profile Detector
*
* Determines profile type (settings-based vs account-based) for routing.
* Priority: settings-based profiles (glm/kimi) checked FIRST for backward compatibility.
*/
class ProfileDetector {
constructor() {
this.configPath = path.join(os.homedir(), '.ccs', 'config.json');
this.profilesPath = path.join(os.homedir(), '.ccs', 'profiles.json');
}
/**
* Read settings-based config (config.json)
* @returns {Object} Config data
*/
_readConfig() {
if (!fs.existsSync(this.configPath)) {
return { profiles: {} };
}
try {
const data = fs.readFileSync(this.configPath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.warn(`[!] Warning: Could not read config.json: ${error.message}`);
return { profiles: {} };
}
}
/**
* Read account-based profiles (profiles.json)
* @returns {Object} Profiles data
*/
_readProfiles() {
if (!fs.existsSync(this.profilesPath)) {
return { profiles: {}, default: null };
}
try {
const data = fs.readFileSync(this.profilesPath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.warn(`[!] Warning: Could not read profiles.json: ${error.message}`);
return { profiles: {}, default: null };
}
}
/**
* Detect profile type and return routing information
* @param {string} profileName - Profile name to detect
* @returns {Object} {type: 'settings'|'account'|'default', ...info}
*/
detectProfileType(profileName) {
// Special case: 'default' means use default profile
if (profileName === 'default' || profileName === null || profileName === undefined) {
return this._resolveDefaultProfile();
}
// Priority 1: Check settings-based profiles (glm, kimi) - BACKWARD COMPATIBILITY
const config = this._readConfig();
if (config.profiles && config.profiles[profileName]) {
return {
type: 'settings',
name: profileName,
settingsPath: config.profiles[profileName]
};
}
// Priority 2: Check account-based profiles (work, personal)
const profiles = this._readProfiles();
if (profiles.profiles && profiles.profiles[profileName]) {
return {
type: 'account',
name: profileName,
profile: profiles.profiles[profileName]
};
}
// Not found - generate suggestions
const allProfiles = this.getAllProfiles();
const allProfileNames = [...allProfiles.settings, ...allProfiles.accounts];
const suggestions = findSimilarStrings(profileName, allProfileNames);
const error = new Error(`Profile not found: ${profileName}`);
error.profileName = profileName;
error.suggestions = suggestions;
error.availableProfiles = this._listAvailableProfiles();
throw error;
}
/**
* Resolve default profile
* @returns {Object} Default profile info
*/
_resolveDefaultProfile() {
// Check if account-based default exists
const profiles = this._readProfiles();
if (profiles.default && profiles.profiles[profiles.default]) {
return {
type: 'account',
name: profiles.default,
profile: profiles.profiles[profiles.default]
};
}
// Check if settings-based default exists
const config = this._readConfig();
if (config.profiles && config.profiles['default']) {
return {
type: 'settings',
name: 'default',
settingsPath: config.profiles['default']
};
}
// No default profile configured, use Claude's own defaults
return {
type: 'default',
name: 'default',
message: 'No profile configured. Using Claude CLI defaults from ~/.claude/'
};
}
/**
* List available profiles (for error messages)
* @returns {string} Formatted list
*/
_listAvailableProfiles() {
const lines = [];
// Settings-based profiles
const config = this._readConfig();
const settingsProfiles = Object.keys(config.profiles || {});
if (settingsProfiles.length > 0) {
lines.push('Settings-based profiles (GLM, Kimi, etc.):');
settingsProfiles.forEach(name => {
lines.push(` - ${name}`);
});
}
// Account-based profiles
const profiles = this._readProfiles();
const accountProfiles = Object.keys(profiles.profiles || {});
if (accountProfiles.length > 0) {
lines.push('Account-based profiles:');
accountProfiles.forEach(name => {
const isDefault = name === profiles.default;
lines.push(` - ${name}${isDefault ? ' [DEFAULT]' : ''}`);
});
}
if (lines.length === 0) {
return ' (no profiles configured)\n' +
' Run "ccs auth save <profile>" to create your first account profile.';
}
return lines.join('\n');
}
/**
* Check if profile exists (any type)
* @param {string} profileName - Profile name
* @returns {boolean} True if exists
*/
hasProfile(profileName) {
try {
this.detectProfileType(profileName);
return true;
} catch {
return false;
}
}
/**
* Get all available profile names
* @returns {Object} {settings: [...], accounts: [...]}
*/
getAllProfiles() {
const config = this._readConfig();
const profiles = this._readProfiles();
return {
settings: Object.keys(config.profiles || {}),
accounts: Object.keys(profiles.profiles || {}),
default: profiles.default
};
}
}
module.exports = ProfileDetector;