mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 16:19:27 +00:00
refactor(auth): modularize auth-commands into commands/ directory
- extract create, list, show, remove, default command handlers - extract types.ts with CommandContext and parseArgs - slim auth-commands.ts from 725 to 235 lines (68% reduction) - add barrel exports at commands/index.ts and auth/index.ts
This commit is contained in:
+42
-532
@@ -1,70 +1,37 @@
|
|||||||
/**
|
/**
|
||||||
* Auth Commands (Simplified)
|
* Auth Commands (Facade)
|
||||||
*
|
*
|
||||||
* CLI interface for CCS multi-account management.
|
* CLI interface for CCS multi-account management.
|
||||||
* Commands: create, list, show, remove, default
|
* Commands: create, list, show, remove, default, reset-default
|
||||||
*
|
*
|
||||||
* Login-per-profile model: Each profile is an isolated Claude instance.
|
* Login-per-profile model: Each profile is an isolated Claude instance.
|
||||||
* Users login directly in each instance (no credential copying).
|
* Users login directly in each instance (no credential copying).
|
||||||
*
|
*
|
||||||
* Supports dual-mode configuration:
|
* Implementation Note: This is a facade that delegates to modular command handlers.
|
||||||
* - Unified YAML format (config.yaml) when CCS_UNIFIED_CONFIG=1 or config.yaml exists
|
* See ./commands/ for individual command implementations.
|
||||||
* - Legacy JSON format (profiles.json) as fallback
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn, ChildProcess } from 'child_process';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import ProfileRegistry from './profile-registry';
|
import ProfileRegistry from './profile-registry';
|
||||||
import { ProfileMetadata } from '../types';
|
|
||||||
import { InstanceManager } from '../management/instance-manager';
|
import { InstanceManager } from '../management/instance-manager';
|
||||||
import {
|
import { initUI, header, subheader, color, dim, warn, fail } from '../utils/ui';
|
||||||
initUI,
|
|
||||||
header,
|
|
||||||
subheader,
|
|
||||||
color,
|
|
||||||
dim,
|
|
||||||
ok,
|
|
||||||
fail,
|
|
||||||
warn,
|
|
||||||
info,
|
|
||||||
table,
|
|
||||||
infoBox,
|
|
||||||
warnBox,
|
|
||||||
} from '../utils/ui';
|
|
||||||
import { getClaudeCliInfo } from '../utils/claude-detector';
|
|
||||||
import { escapeShellArg } from '../utils/shell-executor';
|
|
||||||
import { InteractivePrompt } from '../utils/prompt';
|
|
||||||
import packageJson from '../../package.json';
|
import packageJson from '../../package.json';
|
||||||
import { isUnifiedMode } from '../config/unified-config-loader';
|
|
||||||
import { exitWithError } from '../errors';
|
|
||||||
import { ExitCode } from '../errors/exit-codes';
|
|
||||||
|
|
||||||
interface AuthCommandArgs {
|
// Import command handlers from modular structure
|
||||||
profileName?: string;
|
import {
|
||||||
force?: boolean;
|
type CommandContext,
|
||||||
verbose?: boolean;
|
handleCreate,
|
||||||
json?: boolean;
|
handleList,
|
||||||
yes?: boolean;
|
handleShow,
|
||||||
}
|
handleRemove,
|
||||||
|
handleDefault,
|
||||||
interface ProfileOutput {
|
handleResetDefault,
|
||||||
name: string;
|
} from './commands';
|
||||||
type: string;
|
|
||||||
is_default: boolean;
|
|
||||||
created: string;
|
|
||||||
last_used: string | null;
|
|
||||||
instance_path?: string;
|
|
||||||
session_count?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ListOutput {
|
|
||||||
version: string;
|
|
||||||
profiles: ProfileOutput[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auth Commands Class
|
* Auth Commands Class (Facade)
|
||||||
|
*
|
||||||
|
* Maintains class API for backward compatibility while delegating
|
||||||
|
* to modular command handlers.
|
||||||
*/
|
*/
|
||||||
class AuthCommands {
|
class AuthCommands {
|
||||||
private registry: ProfileRegistry;
|
private registry: ProfileRegistry;
|
||||||
@@ -76,6 +43,17 @@ class AuthCommands {
|
|||||||
this.instanceMgr = new InstanceManager();
|
this.instanceMgr = new InstanceManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get command context for handlers
|
||||||
|
*/
|
||||||
|
private getContext(): CommandContext {
|
||||||
|
return {
|
||||||
|
registry: this.registry,
|
||||||
|
instanceMgr: this.instanceMgr,
|
||||||
|
version: this.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show help for auth commands
|
* Show help for auth commands
|
||||||
*/
|
*/
|
||||||
@@ -138,513 +116,45 @@ class AuthCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse command arguments
|
* Create new profile - delegates to create-command.ts
|
||||||
*/
|
|
||||||
private parseArgs(args: string[]): AuthCommandArgs {
|
|
||||||
const profileName = args.find((arg) => !arg.startsWith('--'));
|
|
||||||
return {
|
|
||||||
profileName,
|
|
||||||
force: args.includes('--force'),
|
|
||||||
verbose: args.includes('--verbose'),
|
|
||||||
json: args.includes('--json'),
|
|
||||||
yes: args.includes('--yes') || args.includes('-y'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new profile and prompt for login
|
|
||||||
*/
|
*/
|
||||||
async handleCreate(args: string[]): Promise<void> {
|
async handleCreate(args: string[]): Promise<void> {
|
||||||
await initUI();
|
return handleCreate(this.getContext(), args);
|
||||||
const { profileName, force } = this.parseArgs(args);
|
|
||||||
|
|
||||||
if (!profileName) {
|
|
||||||
console.log(fail('Profile name is required'));
|
|
||||||
console.log('');
|
|
||||||
console.log(`Usage: ${color('ccs auth create <profile> [--force]', 'command')}`);
|
|
||||||
console.log('');
|
|
||||||
console.log('Example:');
|
|
||||||
console.log(` ${color('ccs auth create work', 'command')}`);
|
|
||||||
exitWithError('Profile name is required', ExitCode.PROFILE_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if profile already exists (check both legacy and unified)
|
|
||||||
const existsLegacy = this.registry.hasProfile(profileName);
|
|
||||||
const existsUnified = this.registry.hasAccountUnified(profileName);
|
|
||||||
if (!force && (existsLegacy || existsUnified)) {
|
|
||||||
console.log(fail(`Profile already exists: ${profileName}`));
|
|
||||||
console.log(` Use ${color('--force', 'command')} to overwrite`);
|
|
||||||
exitWithError(`Profile already exists: ${profileName}`, ExitCode.PROFILE_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create instance directory
|
|
||||||
console.log(info(`Creating profile: ${profileName}`));
|
|
||||||
const instancePath = this.instanceMgr.ensureInstance(profileName);
|
|
||||||
|
|
||||||
// Create/update profile entry based on config mode
|
|
||||||
if (isUnifiedMode()) {
|
|
||||||
// Use unified config (config.yaml)
|
|
||||||
if (existsUnified) {
|
|
||||||
this.registry.touchAccountUnified(profileName);
|
|
||||||
} else {
|
|
||||||
this.registry.createAccountUnified(profileName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use legacy profiles.json
|
|
||||||
if (existsLegacy) {
|
|
||||||
this.registry.updateProfile(profileName, {
|
|
||||||
type: 'account',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.registry.createProfile(profileName, {
|
|
||||||
type: 'account',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(info(`Instance directory: ${instancePath}`));
|
|
||||||
console.log('');
|
|
||||||
console.log(warn('Starting Claude in isolated instance...'));
|
|
||||||
console.log(warn('You will be prompted to login with your account.'));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Detect Claude CLI
|
|
||||||
const claudeInfo = getClaudeCliInfo();
|
|
||||||
if (!claudeInfo) {
|
|
||||||
console.log(fail('Claude CLI not found'));
|
|
||||||
console.log('');
|
|
||||||
console.log('Please install Claude CLI first:');
|
|
||||||
console.log(` ${color('https://claude.ai/download', 'path')}`);
|
|
||||||
exitWithError('Claude CLI not found', ExitCode.BINARY_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { path: claudeCli, needsShell } = claudeInfo;
|
|
||||||
|
|
||||||
// Execute Claude in isolated instance (will auto-prompt for login if no credentials)
|
|
||||||
// On Windows, .cmd/.bat/.ps1 files need shell: true to execute properly
|
|
||||||
let child: ChildProcess;
|
|
||||||
if (needsShell) {
|
|
||||||
const cmdString = escapeShellArg(claudeCli);
|
|
||||||
child = spawn(cmdString, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
windowsHide: true,
|
|
||||||
shell: true,
|
|
||||||
env: { ...process.env, CLAUDE_CONFIG_DIR: instancePath },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
child = spawn(claudeCli, [], {
|
|
||||||
stdio: 'inherit',
|
|
||||||
windowsHide: true,
|
|
||||||
env: { ...process.env, CLAUDE_CONFIG_DIR: instancePath },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
child.on('exit', (code: number | null) => {
|
|
||||||
if (code === 0) {
|
|
||||||
console.log('');
|
|
||||||
console.log(
|
|
||||||
infoBox(
|
|
||||||
`Profile: ${profileName}\n` + `Instance: ${instancePath}\n` + `Type: account`,
|
|
||||||
'Profile Created'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
console.log(header('Usage'));
|
|
||||||
console.log(` ${color(`ccs ${profileName} "your prompt here"`, 'command')}`);
|
|
||||||
console.log('');
|
|
||||||
console.log(
|
|
||||||
warnBox(
|
|
||||||
`Running the command below will SWITCH your default\n` +
|
|
||||||
`CCS account to "${profileName}". After this, running\n` +
|
|
||||||
`"ccs" without a profile name will use this account.\n\n` +
|
|
||||||
` ${color(`ccs auth default ${profileName}`, 'command')}\n\n` +
|
|
||||||
`To restore the original default, run:\n` +
|
|
||||||
` ${color('ccs auth reset-default', 'command')}`,
|
|
||||||
'Set as Default?'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
console.log('');
|
|
||||||
console.log(fail('Login failed or cancelled'));
|
|
||||||
console.log('');
|
|
||||||
console.log('To retry:');
|
|
||||||
console.log(` ${color(`ccs auth create ${profileName} --force`, 'command')}`);
|
|
||||||
console.log('');
|
|
||||||
exitWithError('Login failed or cancelled', ExitCode.AUTH_ERROR);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', (err: Error) => {
|
|
||||||
exitWithError(`Failed to execute Claude CLI: ${err.message}`, ExitCode.BINARY_ERROR);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
exitWithError(
|
|
||||||
`Failed to create profile: ${(error as Error).message}`,
|
|
||||||
ExitCode.GENERAL_ERROR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format relative time (e.g., "2h ago", "1d ago")
|
* List all profiles - delegates to list-command.ts
|
||||||
*/
|
|
||||||
private formatRelativeTime(date: Date): string {
|
|
||||||
const now = Date.now();
|
|
||||||
const diff = now - date.getTime();
|
|
||||||
|
|
||||||
const minutes = Math.floor(diff / 60000);
|
|
||||||
const hours = Math.floor(diff / 3600000);
|
|
||||||
const days = Math.floor(diff / 86400000);
|
|
||||||
|
|
||||||
if (days > 0) return `${days}d ago`;
|
|
||||||
if (hours > 0) return `${hours}h ago`;
|
|
||||||
if (minutes > 0) return `${minutes}m ago`;
|
|
||||||
return 'just now';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List all saved profiles
|
|
||||||
*/
|
*/
|
||||||
async handleList(args: string[]): Promise<void> {
|
async handleList(args: string[]): Promise<void> {
|
||||||
await initUI();
|
return handleList(this.getContext(), args);
|
||||||
const { verbose, json } = this.parseArgs(args);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get profiles from both legacy (profiles.json) and unified config (config.yaml)
|
|
||||||
const legacyProfiles = this.registry.getAllProfiles();
|
|
||||||
const unifiedAccounts = this.registry.getAllAccountsUnified();
|
|
||||||
|
|
||||||
// Merge profiles: unified config takes precedence
|
|
||||||
const profiles: Record<string, ProfileMetadata> = { ...legacyProfiles };
|
|
||||||
for (const [name, account] of Object.entries(unifiedAccounts)) {
|
|
||||||
profiles[name] = {
|
|
||||||
type: 'account',
|
|
||||||
created: account.created,
|
|
||||||
last_used: account.last_used,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProfile = this.registry.getDefaultUnified() ?? this.registry.getDefaultProfile();
|
|
||||||
const profileNames = Object.keys(profiles);
|
|
||||||
|
|
||||||
// JSON output mode
|
|
||||||
if (json) {
|
|
||||||
const output: ListOutput = {
|
|
||||||
version: this.version,
|
|
||||||
profiles: profileNames.map((name) => {
|
|
||||||
const profile = profiles[name];
|
|
||||||
const isDefault = name === defaultProfile;
|
|
||||||
const instancePath = this.instanceMgr.getInstancePath(name);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
type: profile.type || 'account',
|
|
||||||
is_default: isDefault,
|
|
||||||
created: profile.created,
|
|
||||||
last_used: profile.last_used || null,
|
|
||||||
instance_path: instancePath,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
console.log(JSON.stringify(output, null, 2));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Human-readable output
|
|
||||||
if (profileNames.length === 0) {
|
|
||||||
console.log(warn('No account profiles found'));
|
|
||||||
console.log('');
|
|
||||||
console.log('To create your first profile:');
|
|
||||||
console.log(` ${color('ccs auth create <profile>', 'command')}`);
|
|
||||||
console.log('');
|
|
||||||
console.log('Example:');
|
|
||||||
console.log(` ${color('ccs auth create work', 'command')}`);
|
|
||||||
console.log('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(header('Saved Account Profiles'));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Sort by last_used (descending), then alphabetically
|
|
||||||
const sorted = profileNames.sort((a, b) => {
|
|
||||||
const aProfile = profiles[a];
|
|
||||||
const bProfile = profiles[b];
|
|
||||||
|
|
||||||
// Default first
|
|
||||||
if (a === defaultProfile) return -1;
|
|
||||||
if (b === defaultProfile) return 1;
|
|
||||||
|
|
||||||
// Then by last_used
|
|
||||||
if (aProfile.last_used && bProfile.last_used) {
|
|
||||||
return new Date(bProfile.last_used).getTime() - new Date(aProfile.last_used).getTime();
|
|
||||||
}
|
|
||||||
if (aProfile.last_used) return -1;
|
|
||||||
if (bProfile.last_used) return 1;
|
|
||||||
|
|
||||||
// Then alphabetically
|
|
||||||
return a.localeCompare(b);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build table rows
|
|
||||||
const rows: string[][] = sorted.map((name) => {
|
|
||||||
const profile = profiles[name];
|
|
||||||
const isDefault = name === defaultProfile;
|
|
||||||
|
|
||||||
// Status column
|
|
||||||
const status = isDefault ? color('[OK] default', 'success') : color('[OK]', 'success');
|
|
||||||
|
|
||||||
// Last used column
|
|
||||||
let lastUsed = '-';
|
|
||||||
if (profile.last_used) {
|
|
||||||
lastUsed = this.formatRelativeTime(new Date(profile.last_used));
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = [
|
|
||||||
color(name, isDefault ? 'primary' : 'info'),
|
|
||||||
profile.type || 'account',
|
|
||||||
status,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (verbose) {
|
|
||||||
row.push(lastUsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Headers
|
|
||||||
const headers = verbose
|
|
||||||
? ['Profile', 'Type', 'Status', 'Last Used']
|
|
||||||
: ['Profile', 'Type', 'Status'];
|
|
||||||
|
|
||||||
// Print table
|
|
||||||
console.log(
|
|
||||||
table(rows, {
|
|
||||||
head: headers,
|
|
||||||
colWidths: verbose ? [15, 12, 15, 12] : [15, 12, 15],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
console.log(dim(`Total: ${profileNames.length} profile(s)`));
|
|
||||||
console.log('');
|
|
||||||
} catch (error) {
|
|
||||||
exitWithError(`Failed to list profiles: ${(error as Error).message}`, ExitCode.GENERAL_ERROR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show details for a specific profile
|
* Show profile details - delegates to show-command.ts
|
||||||
*/
|
*/
|
||||||
async handleShow(args: string[]): Promise<void> {
|
async handleShow(args: string[]): Promise<void> {
|
||||||
await initUI();
|
return handleShow(this.getContext(), args);
|
||||||
const { profileName, json } = this.parseArgs(args);
|
|
||||||
|
|
||||||
if (!profileName) {
|
|
||||||
console.log(fail('Profile name is required'));
|
|
||||||
console.log('');
|
|
||||||
console.log(`Usage: ${color('ccs auth show <profile> [--json]', 'command')}`);
|
|
||||||
exitWithError('Profile name is required', ExitCode.PROFILE_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const profile = this.registry.getProfile(profileName);
|
|
||||||
const defaultProfile = this.registry.getDefaultProfile();
|
|
||||||
const isDefault = profileName === defaultProfile;
|
|
||||||
const instancePath = this.instanceMgr.getInstancePath(profileName);
|
|
||||||
|
|
||||||
// Count sessions
|
|
||||||
let sessionCount = 0;
|
|
||||||
try {
|
|
||||||
const sessionsDir = path.join(instancePath, 'session-env');
|
|
||||||
if (fs.existsSync(sessionsDir)) {
|
|
||||||
const files = fs.readdirSync(sessionsDir);
|
|
||||||
sessionCount = files.filter((f) => f.endsWith('.json')).length;
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// Ignore errors counting sessions
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON output mode
|
|
||||||
if (json) {
|
|
||||||
const output: ProfileOutput = {
|
|
||||||
name: profileName,
|
|
||||||
type: profile.type || 'account',
|
|
||||||
is_default: isDefault,
|
|
||||||
created: profile.created,
|
|
||||||
last_used: profile.last_used || null,
|
|
||||||
instance_path: instancePath,
|
|
||||||
session_count: sessionCount,
|
|
||||||
};
|
|
||||||
console.log(JSON.stringify(output, null, 2));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Human-readable output
|
|
||||||
const defaultBadge = isDefault ? color(' (default)', 'success') : '';
|
|
||||||
console.log(header(`Profile: ${profileName}${defaultBadge}`));
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Details table
|
|
||||||
const details = [
|
|
||||||
['Type', profile.type || 'account'],
|
|
||||||
['Instance', instancePath],
|
|
||||||
['Created', new Date(profile.created).toLocaleString()],
|
|
||||||
['Last Used', profile.last_used ? new Date(profile.last_used).toLocaleString() : 'Never'],
|
|
||||||
['Sessions', `${sessionCount}`],
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
table(details, {
|
|
||||||
colWidths: [15, 45],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
} catch (error) {
|
|
||||||
exitWithError((error as Error).message, ExitCode.PROFILE_ERROR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a saved profile
|
* Remove profile - delegates to remove-command.ts
|
||||||
*/
|
*/
|
||||||
async handleRemove(args: string[]): Promise<void> {
|
async handleRemove(args: string[]): Promise<void> {
|
||||||
await initUI();
|
return handleRemove(this.getContext(), args);
|
||||||
const { profileName, yes } = this.parseArgs(args);
|
|
||||||
|
|
||||||
if (!profileName) {
|
|
||||||
console.log(fail('Profile name is required'));
|
|
||||||
console.log('');
|
|
||||||
console.log(`Usage: ${color('ccs auth remove <profile> [--yes]', 'command')}`);
|
|
||||||
exitWithError('Profile name is required', ExitCode.PROFILE_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check existence in both legacy and unified
|
|
||||||
const existsLegacy = this.registry.hasProfile(profileName);
|
|
||||||
const existsUnified = this.registry.hasAccountUnified(profileName);
|
|
||||||
|
|
||||||
if (!existsLegacy && !existsUnified) {
|
|
||||||
console.log(fail(`Profile not found: ${profileName}`));
|
|
||||||
exitWithError(`Profile not found: ${profileName}`, ExitCode.PROFILE_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get instance path and session count for impact display
|
|
||||||
const instancePath = this.instanceMgr.getInstancePath(profileName);
|
|
||||||
let sessionCount = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sessionsDir = path.join(instancePath, 'session-env');
|
|
||||||
if (fs.existsSync(sessionsDir)) {
|
|
||||||
const files = fs.readdirSync(sessionsDir);
|
|
||||||
sessionCount = files.filter((f) => f.endsWith('.json')).length;
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// Ignore errors counting sessions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display impact
|
|
||||||
console.log('');
|
|
||||||
console.log(`Profile '${color(profileName, 'command')}' will be permanently deleted.`);
|
|
||||||
console.log(` Instance path: ${instancePath}`);
|
|
||||||
console.log(` Sessions: ${sessionCount} conversation${sessionCount !== 1 ? 's' : ''}`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Interactive confirmation (or --yes flag)
|
|
||||||
const confirmed =
|
|
||||||
yes ||
|
|
||||||
(await InteractivePrompt.confirm(
|
|
||||||
'Delete this profile?',
|
|
||||||
{ default: false } // Default to NO (safe)
|
|
||||||
));
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
console.log(info('Cancelled'));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete instance
|
|
||||||
this.instanceMgr.deleteInstance(profileName);
|
|
||||||
|
|
||||||
// Delete profile from appropriate config
|
|
||||||
if (isUnifiedMode() && existsUnified) {
|
|
||||||
this.registry.removeAccountUnified(profileName);
|
|
||||||
}
|
|
||||||
if (existsLegacy) {
|
|
||||||
this.registry.deleteProfile(profileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(ok(`Profile removed: ${profileName}`));
|
|
||||||
console.log('');
|
|
||||||
} catch (error) {
|
|
||||||
exitWithError(
|
|
||||||
`Failed to remove profile: ${(error as Error).message}`,
|
|
||||||
ExitCode.GENERAL_ERROR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set default profile
|
* Set default profile - delegates to default-command.ts
|
||||||
*/
|
*/
|
||||||
async handleDefault(args: string[]): Promise<void> {
|
async handleDefault(args: string[]): Promise<void> {
|
||||||
await initUI();
|
return handleDefault(this.getContext(), args);
|
||||||
const { profileName } = this.parseArgs(args);
|
|
||||||
|
|
||||||
if (!profileName) {
|
|
||||||
console.log(fail('Profile name is required'));
|
|
||||||
console.log('');
|
|
||||||
console.log(`Usage: ${color('ccs auth default <profile>', 'command')}`);
|
|
||||||
exitWithError('Profile name is required', ExitCode.PROFILE_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use unified or legacy based on config mode
|
|
||||||
if (isUnifiedMode()) {
|
|
||||||
this.registry.setDefaultUnified(profileName);
|
|
||||||
} else {
|
|
||||||
this.registry.setDefaultProfile(profileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(ok(`Default profile set: ${profileName}`));
|
|
||||||
console.log('');
|
|
||||||
console.log('Now you can use:');
|
|
||||||
console.log(
|
|
||||||
` ${color('ccs "your prompt"', 'command')} ${dim(`# Uses ${profileName} profile`)}`
|
|
||||||
);
|
|
||||||
console.log('');
|
|
||||||
} catch (error) {
|
|
||||||
exitWithError((error as Error).message, ExitCode.PROFILE_ERROR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset default profile (clear the custom default, restore original CCS behavior)
|
* Reset default profile - delegates to default-command.ts
|
||||||
*/
|
*/
|
||||||
async handleResetDefault(): Promise<void> {
|
async handleResetDefault(): Promise<void> {
|
||||||
await initUI();
|
return handleResetDefault(this.getContext());
|
||||||
|
|
||||||
try {
|
|
||||||
// Use unified or legacy based on config mode
|
|
||||||
if (isUnifiedMode()) {
|
|
||||||
this.registry.clearDefaultUnified();
|
|
||||||
} else {
|
|
||||||
this.registry.clearDefaultProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(ok('Default profile cleared'));
|
|
||||||
console.log('');
|
|
||||||
console.log('CCS will now use the original behavior:');
|
|
||||||
console.log(` ${dim('# Uses your primary Claude account')}`);
|
|
||||||
console.log(` ${color('ccs "your prompt"', 'command')}`);
|
|
||||||
console.log('');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(fail((error as Error).message));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Create Command Handler
|
||||||
|
*
|
||||||
|
* Creates a new profile and prompts for login in an isolated Claude instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
|
import { initUI, header, color, fail, warn, info, infoBox, warnBox } from '../../utils/ui';
|
||||||
|
import { getClaudeCliInfo } from '../../utils/claude-detector';
|
||||||
|
import { escapeShellArg } from '../../utils/shell-executor';
|
||||||
|
import { isUnifiedMode } from '../../config/unified-config-loader';
|
||||||
|
import { exitWithError } from '../../errors';
|
||||||
|
import { ExitCode } from '../../errors/exit-codes';
|
||||||
|
import { CommandContext, parseArgs } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the create command
|
||||||
|
*/
|
||||||
|
export async function handleCreate(ctx: CommandContext, args: string[]): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
const { profileName, force } = parseArgs(args);
|
||||||
|
|
||||||
|
if (!profileName) {
|
||||||
|
console.log(fail('Profile name is required'));
|
||||||
|
console.log('');
|
||||||
|
console.log(`Usage: ${color('ccs auth create <profile> [--force]', 'command')}`);
|
||||||
|
console.log('');
|
||||||
|
console.log('Example:');
|
||||||
|
console.log(` ${color('ccs auth create work', 'command')}`);
|
||||||
|
exitWithError('Profile name is required', ExitCode.PROFILE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if profile already exists (check both legacy and unified)
|
||||||
|
const existsLegacy = ctx.registry.hasProfile(profileName);
|
||||||
|
const existsUnified = ctx.registry.hasAccountUnified(profileName);
|
||||||
|
if (!force && (existsLegacy || existsUnified)) {
|
||||||
|
console.log(fail(`Profile already exists: ${profileName}`));
|
||||||
|
console.log(` Use ${color('--force', 'command')} to overwrite`);
|
||||||
|
exitWithError(`Profile already exists: ${profileName}`, ExitCode.PROFILE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create instance directory
|
||||||
|
console.log(info(`Creating profile: ${profileName}`));
|
||||||
|
const instancePath = ctx.instanceMgr.ensureInstance(profileName);
|
||||||
|
|
||||||
|
// Create/update profile entry based on config mode
|
||||||
|
if (isUnifiedMode()) {
|
||||||
|
// Use unified config (config.yaml)
|
||||||
|
if (existsUnified) {
|
||||||
|
ctx.registry.touchAccountUnified(profileName);
|
||||||
|
} else {
|
||||||
|
ctx.registry.createAccountUnified(profileName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use legacy profiles.json
|
||||||
|
if (existsLegacy) {
|
||||||
|
ctx.registry.updateProfile(profileName, {
|
||||||
|
type: 'account',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ctx.registry.createProfile(profileName, {
|
||||||
|
type: 'account',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(info(`Instance directory: ${instancePath}`));
|
||||||
|
console.log('');
|
||||||
|
console.log(warn('Starting Claude in isolated instance...'));
|
||||||
|
console.log(warn('You will be prompted to login with your account.'));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Detect Claude CLI
|
||||||
|
const claudeInfo = getClaudeCliInfo();
|
||||||
|
if (!claudeInfo) {
|
||||||
|
console.log(fail('Claude CLI not found'));
|
||||||
|
console.log('');
|
||||||
|
console.log('Please install Claude CLI first:');
|
||||||
|
console.log(` ${color('https://claude.ai/download', 'path')}`);
|
||||||
|
exitWithError('Claude CLI not found', ExitCode.BINARY_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { path: claudeCli, needsShell } = claudeInfo;
|
||||||
|
|
||||||
|
// Execute Claude in isolated instance (will auto-prompt for login if no credentials)
|
||||||
|
// On Windows, .cmd/.bat/.ps1 files need shell: true to execute properly
|
||||||
|
let child: ChildProcess;
|
||||||
|
if (needsShell) {
|
||||||
|
const cmdString = escapeShellArg(claudeCli);
|
||||||
|
child = spawn(cmdString, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
windowsHide: true,
|
||||||
|
shell: true,
|
||||||
|
env: { ...process.env, CLAUDE_CONFIG_DIR: instancePath },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
child = spawn(claudeCli, [], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
windowsHide: true,
|
||||||
|
env: { ...process.env, CLAUDE_CONFIG_DIR: instancePath },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
child.on('exit', (code: number | null) => {
|
||||||
|
if (code === 0) {
|
||||||
|
console.log('');
|
||||||
|
console.log(
|
||||||
|
infoBox(
|
||||||
|
`Profile: ${profileName}\n` + `Instance: ${instancePath}\n` + `Type: account`,
|
||||||
|
'Profile Created'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
console.log(header('Usage'));
|
||||||
|
console.log(` ${color(`ccs ${profileName} "your prompt here"`, 'command')}`);
|
||||||
|
console.log('');
|
||||||
|
console.log(
|
||||||
|
warnBox(
|
||||||
|
`Running the command below will SWITCH your default\n` +
|
||||||
|
`CCS account to "${profileName}". After this, running\n` +
|
||||||
|
`"ccs" without a profile name will use this account.\n\n` +
|
||||||
|
` ${color(`ccs auth default ${profileName}`, 'command')}\n\n` +
|
||||||
|
`To restore the original default, run:\n` +
|
||||||
|
` ${color('ccs auth reset-default', 'command')}`,
|
||||||
|
'Set as Default?'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log('');
|
||||||
|
console.log(fail('Login failed or cancelled'));
|
||||||
|
console.log('');
|
||||||
|
console.log('To retry:');
|
||||||
|
console.log(` ${color(`ccs auth create ${profileName} --force`, 'command')}`);
|
||||||
|
console.log('');
|
||||||
|
exitWithError('Login failed or cancelled', ExitCode.AUTH_ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err: Error) => {
|
||||||
|
exitWithError(`Failed to execute Claude CLI: ${err.message}`, ExitCode.BINARY_ERROR);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
exitWithError(`Failed to create profile: ${(error as Error).message}`, ExitCode.GENERAL_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Default Command Handler
|
||||||
|
*
|
||||||
|
* Sets or clears the default profile.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { initUI, color, dim, ok, fail } from '../../utils/ui';
|
||||||
|
import { isUnifiedMode } from '../../config/unified-config-loader';
|
||||||
|
import { exitWithError } from '../../errors';
|
||||||
|
import { ExitCode } from '../../errors/exit-codes';
|
||||||
|
import { CommandContext, parseArgs } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the default command (set default profile)
|
||||||
|
*/
|
||||||
|
export async function handleDefault(ctx: CommandContext, args: string[]): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
const { profileName } = parseArgs(args);
|
||||||
|
|
||||||
|
if (!profileName) {
|
||||||
|
console.log(fail('Profile name is required'));
|
||||||
|
console.log('');
|
||||||
|
console.log(`Usage: ${color('ccs auth default <profile>', 'command')}`);
|
||||||
|
exitWithError('Profile name is required', ExitCode.PROFILE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use unified or legacy based on config mode
|
||||||
|
if (isUnifiedMode()) {
|
||||||
|
ctx.registry.setDefaultUnified(profileName);
|
||||||
|
} else {
|
||||||
|
ctx.registry.setDefaultProfile(profileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(ok(`Default profile set: ${profileName}`));
|
||||||
|
console.log('');
|
||||||
|
console.log('Now you can use:');
|
||||||
|
console.log(
|
||||||
|
` ${color('ccs "your prompt"', 'command')} ${dim(`# Uses ${profileName} profile`)}`
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
} catch (error) {
|
||||||
|
exitWithError((error as Error).message, ExitCode.PROFILE_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the reset-default command (clear the custom default)
|
||||||
|
*/
|
||||||
|
export async function handleResetDefault(ctx: CommandContext): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use unified or legacy based on config mode
|
||||||
|
if (isUnifiedMode()) {
|
||||||
|
ctx.registry.clearDefaultUnified();
|
||||||
|
} else {
|
||||||
|
ctx.registry.clearDefaultProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(ok('Default profile cleared'));
|
||||||
|
console.log('');
|
||||||
|
console.log('CCS will now use the original behavior:');
|
||||||
|
console.log(` ${dim('# Uses your primary Claude account')}`);
|
||||||
|
console.log(` ${color('ccs "your prompt"', 'command')}`);
|
||||||
|
console.log('');
|
||||||
|
} catch (error) {
|
||||||
|
console.log(fail((error as Error).message));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Auth Commands Barrel Export
|
||||||
|
*
|
||||||
|
* Re-exports all command handlers and types from the commands module.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Types and utilities
|
||||||
|
export {
|
||||||
|
AuthCommandArgs,
|
||||||
|
ProfileOutput,
|
||||||
|
ListOutput,
|
||||||
|
CommandContext,
|
||||||
|
parseArgs,
|
||||||
|
formatRelativeTime,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// Command handlers
|
||||||
|
export { handleCreate } from './create-command';
|
||||||
|
export { handleList } from './list-command';
|
||||||
|
export { handleShow } from './show-command';
|
||||||
|
export { handleRemove } from './remove-command';
|
||||||
|
export { handleDefault, handleResetDefault } from './default-command';
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* List Command Handler
|
||||||
|
*
|
||||||
|
* Lists all saved profiles from both legacy and unified config.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ProfileMetadata } from '../../types';
|
||||||
|
import { initUI, header, color, dim, warn, table } from '../../utils/ui';
|
||||||
|
import { exitWithError } from '../../errors';
|
||||||
|
import { ExitCode } from '../../errors/exit-codes';
|
||||||
|
import { CommandContext, ListOutput, parseArgs, formatRelativeTime } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the list command
|
||||||
|
*/
|
||||||
|
export async function handleList(ctx: CommandContext, args: string[]): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
const { verbose, json } = parseArgs(args);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get profiles from both legacy (profiles.json) and unified config (config.yaml)
|
||||||
|
const legacyProfiles = ctx.registry.getAllProfiles();
|
||||||
|
const unifiedAccounts = ctx.registry.getAllAccountsUnified();
|
||||||
|
|
||||||
|
// Merge profiles: unified config takes precedence
|
||||||
|
const profiles: Record<string, ProfileMetadata> = { ...legacyProfiles };
|
||||||
|
for (const [name, account] of Object.entries(unifiedAccounts)) {
|
||||||
|
profiles[name] = {
|
||||||
|
type: 'account',
|
||||||
|
created: account.created,
|
||||||
|
last_used: account.last_used,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProfile = ctx.registry.getDefaultUnified() ?? ctx.registry.getDefaultProfile();
|
||||||
|
const profileNames = Object.keys(profiles);
|
||||||
|
|
||||||
|
// JSON output mode
|
||||||
|
if (json) {
|
||||||
|
const output: ListOutput = {
|
||||||
|
version: ctx.version,
|
||||||
|
profiles: profileNames.map((name) => {
|
||||||
|
const profile = profiles[name];
|
||||||
|
const isDefault = name === defaultProfile;
|
||||||
|
const instancePath = ctx.instanceMgr.getInstancePath(name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
type: profile.type || 'account',
|
||||||
|
is_default: isDefault,
|
||||||
|
created: profile.created,
|
||||||
|
last_used: profile.last_used || null,
|
||||||
|
instance_path: instancePath,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(output, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human-readable output
|
||||||
|
if (profileNames.length === 0) {
|
||||||
|
console.log(warn('No account profiles found'));
|
||||||
|
console.log('');
|
||||||
|
console.log('To create your first profile:');
|
||||||
|
console.log(` ${color('ccs auth create <profile>', 'command')}`);
|
||||||
|
console.log('');
|
||||||
|
console.log('Example:');
|
||||||
|
console.log(` ${color('ccs auth create work', 'command')}`);
|
||||||
|
console.log('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(header('Saved Account Profiles'));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Sort by last_used (descending), then alphabetically
|
||||||
|
const sorted = profileNames.sort((a, b) => {
|
||||||
|
const aProfile = profiles[a];
|
||||||
|
const bProfile = profiles[b];
|
||||||
|
|
||||||
|
// Default first
|
||||||
|
if (a === defaultProfile) return -1;
|
||||||
|
if (b === defaultProfile) return 1;
|
||||||
|
|
||||||
|
// Then by last_used
|
||||||
|
if (aProfile.last_used && bProfile.last_used) {
|
||||||
|
return new Date(bProfile.last_used).getTime() - new Date(aProfile.last_used).getTime();
|
||||||
|
}
|
||||||
|
if (aProfile.last_used) return -1;
|
||||||
|
if (bProfile.last_used) return 1;
|
||||||
|
|
||||||
|
// Then alphabetically
|
||||||
|
return a.localeCompare(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build table rows
|
||||||
|
const rows: string[][] = sorted.map((name) => {
|
||||||
|
const profile = profiles[name];
|
||||||
|
const isDefault = name === defaultProfile;
|
||||||
|
|
||||||
|
// Status column
|
||||||
|
const status = isDefault ? color('[OK] default', 'success') : color('[OK]', 'success');
|
||||||
|
|
||||||
|
// Last used column
|
||||||
|
let lastUsed = '-';
|
||||||
|
if (profile.last_used) {
|
||||||
|
lastUsed = formatRelativeTime(new Date(profile.last_used));
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = [color(name, isDefault ? 'primary' : 'info'), profile.type || 'account', status];
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
row.push(lastUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = verbose
|
||||||
|
? ['Profile', 'Type', 'Status', 'Last Used']
|
||||||
|
: ['Profile', 'Type', 'Status'];
|
||||||
|
|
||||||
|
// Print table
|
||||||
|
console.log(
|
||||||
|
table(rows, {
|
||||||
|
head: headers,
|
||||||
|
colWidths: verbose ? [15, 12, 15, 12] : [15, 12, 15],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
console.log(dim(`Total: ${profileNames.length} profile(s)`));
|
||||||
|
console.log('');
|
||||||
|
} catch (error) {
|
||||||
|
exitWithError(`Failed to list profiles: ${(error as Error).message}`, ExitCode.GENERAL_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Remove Command Handler
|
||||||
|
*
|
||||||
|
* Removes a saved profile and its instance directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { initUI, color, ok, fail, info } from '../../utils/ui';
|
||||||
|
import { InteractivePrompt } from '../../utils/prompt';
|
||||||
|
import { isUnifiedMode } from '../../config/unified-config-loader';
|
||||||
|
import { exitWithError } from '../../errors';
|
||||||
|
import { ExitCode } from '../../errors/exit-codes';
|
||||||
|
import { CommandContext, parseArgs } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the remove command
|
||||||
|
*/
|
||||||
|
export async function handleRemove(ctx: CommandContext, args: string[]): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
const { profileName, yes } = parseArgs(args);
|
||||||
|
|
||||||
|
if (!profileName) {
|
||||||
|
console.log(fail('Profile name is required'));
|
||||||
|
console.log('');
|
||||||
|
console.log(`Usage: ${color('ccs auth remove <profile> [--yes]', 'command')}`);
|
||||||
|
exitWithError('Profile name is required', ExitCode.PROFILE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check existence in both legacy and unified
|
||||||
|
const existsLegacy = ctx.registry.hasProfile(profileName);
|
||||||
|
const existsUnified = ctx.registry.hasAccountUnified(profileName);
|
||||||
|
|
||||||
|
if (!existsLegacy && !existsUnified) {
|
||||||
|
console.log(fail(`Profile not found: ${profileName}`));
|
||||||
|
exitWithError(`Profile not found: ${profileName}`, ExitCode.PROFILE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get instance path and session count for impact display
|
||||||
|
const instancePath = ctx.instanceMgr.getInstancePath(profileName);
|
||||||
|
let sessionCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionsDir = path.join(instancePath, 'session-env');
|
||||||
|
if (fs.existsSync(sessionsDir)) {
|
||||||
|
const files = fs.readdirSync(sessionsDir);
|
||||||
|
sessionCount = files.filter((f) => f.endsWith('.json')).length;
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// Ignore errors counting sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display impact
|
||||||
|
console.log('');
|
||||||
|
console.log(`Profile '${color(profileName, 'command')}' will be permanently deleted.`);
|
||||||
|
console.log(` Instance path: ${instancePath}`);
|
||||||
|
console.log(` Sessions: ${sessionCount} conversation${sessionCount !== 1 ? 's' : ''}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Interactive confirmation (or --yes flag)
|
||||||
|
const confirmed =
|
||||||
|
yes || (await InteractivePrompt.confirm('Delete this profile?', { default: false })); // Default to NO (safe)
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
console.log(info('Cancelled'));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete instance
|
||||||
|
ctx.instanceMgr.deleteInstance(profileName);
|
||||||
|
|
||||||
|
// Delete profile from appropriate config
|
||||||
|
if (isUnifiedMode() && existsUnified) {
|
||||||
|
ctx.registry.removeAccountUnified(profileName);
|
||||||
|
}
|
||||||
|
if (existsLegacy) {
|
||||||
|
ctx.registry.deleteProfile(profileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(ok(`Profile removed: ${profileName}`));
|
||||||
|
console.log('');
|
||||||
|
} catch (error) {
|
||||||
|
exitWithError(`Failed to remove profile: ${(error as Error).message}`, ExitCode.GENERAL_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Show Command Handler
|
||||||
|
*
|
||||||
|
* Shows details for a specific profile.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { initUI, header, color, fail, table } from '../../utils/ui';
|
||||||
|
import { exitWithError } from '../../errors';
|
||||||
|
import { ExitCode } from '../../errors/exit-codes';
|
||||||
|
import { CommandContext, ProfileOutput, parseArgs } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the show command
|
||||||
|
*/
|
||||||
|
export async function handleShow(ctx: CommandContext, args: string[]): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
const { profileName, json } = parseArgs(args);
|
||||||
|
|
||||||
|
if (!profileName) {
|
||||||
|
console.log(fail('Profile name is required'));
|
||||||
|
console.log('');
|
||||||
|
console.log(`Usage: ${color('ccs auth show <profile> [--json]', 'command')}`);
|
||||||
|
exitWithError('Profile name is required', ExitCode.PROFILE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const profile = ctx.registry.getProfile(profileName);
|
||||||
|
const defaultProfile = ctx.registry.getDefaultProfile();
|
||||||
|
const isDefault = profileName === defaultProfile;
|
||||||
|
const instancePath = ctx.instanceMgr.getInstancePath(profileName);
|
||||||
|
|
||||||
|
// Count sessions
|
||||||
|
let sessionCount = 0;
|
||||||
|
try {
|
||||||
|
const sessionsDir = path.join(instancePath, 'session-env');
|
||||||
|
if (fs.existsSync(sessionsDir)) {
|
||||||
|
const files = fs.readdirSync(sessionsDir);
|
||||||
|
sessionCount = files.filter((f) => f.endsWith('.json')).length;
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// Ignore errors counting sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON output mode
|
||||||
|
if (json) {
|
||||||
|
const output: ProfileOutput = {
|
||||||
|
name: profileName,
|
||||||
|
type: profile.type || 'account',
|
||||||
|
is_default: isDefault,
|
||||||
|
created: profile.created,
|
||||||
|
last_used: profile.last_used || null,
|
||||||
|
instance_path: instancePath,
|
||||||
|
session_count: sessionCount,
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(output, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human-readable output
|
||||||
|
const defaultBadge = isDefault ? color(' (default)', 'success') : '';
|
||||||
|
console.log(header(`Profile: ${profileName}${defaultBadge}`));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Details table
|
||||||
|
const details = [
|
||||||
|
['Type', profile.type || 'account'],
|
||||||
|
['Instance', instancePath],
|
||||||
|
['Created', new Date(profile.created).toLocaleString()],
|
||||||
|
['Last Used', profile.last_used ? new Date(profile.last_used).toLocaleString() : 'Never'],
|
||||||
|
['Sessions', `${sessionCount}`],
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
table(details, {
|
||||||
|
colWidths: [15, 45],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
} catch (error) {
|
||||||
|
exitWithError((error as Error).message, ExitCode.PROFILE_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Auth Commands Type Definitions
|
||||||
|
*
|
||||||
|
* Shared interfaces for auth command modules.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ProfileRegistry from '../profile-registry';
|
||||||
|
import { InstanceManager } from '../../management/instance-manager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command arguments parsed from CLI
|
||||||
|
*/
|
||||||
|
export interface AuthCommandArgs {
|
||||||
|
profileName?: string;
|
||||||
|
force?: boolean;
|
||||||
|
verbose?: boolean;
|
||||||
|
json?: boolean;
|
||||||
|
yes?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile output for JSON mode
|
||||||
|
*/
|
||||||
|
export interface ProfileOutput {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
is_default: boolean;
|
||||||
|
created: string;
|
||||||
|
last_used: string | null;
|
||||||
|
instance_path?: string;
|
||||||
|
session_count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List output for JSON mode
|
||||||
|
*/
|
||||||
|
export interface ListOutput {
|
||||||
|
version: string;
|
||||||
|
profiles: ProfileOutput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared context passed to command handlers
|
||||||
|
*/
|
||||||
|
export interface CommandContext {
|
||||||
|
registry: ProfileRegistry;
|
||||||
|
instanceMgr: InstanceManager;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse command arguments from raw args array
|
||||||
|
*/
|
||||||
|
export function parseArgs(args: string[]): AuthCommandArgs {
|
||||||
|
const profileName = args.find((arg) => !arg.startsWith('--'));
|
||||||
|
return {
|
||||||
|
profileName,
|
||||||
|
force: args.includes('--force'),
|
||||||
|
verbose: args.includes('--verbose'),
|
||||||
|
json: args.includes('--json'),
|
||||||
|
yes: args.includes('--yes') || args.includes('-y'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format relative time (e.g., "2h ago", "1d ago")
|
||||||
|
*/
|
||||||
|
export function formatRelativeTime(date: Date): string {
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = now - date.getTime();
|
||||||
|
|
||||||
|
const minutes = Math.floor(diff / 60000);
|
||||||
|
const hours = Math.floor(diff / 3600000);
|
||||||
|
const days = Math.floor(diff / 86400000);
|
||||||
|
|
||||||
|
if (days > 0) return `${days}d ago`;
|
||||||
|
if (hours > 0) return `${hours}h ago`;
|
||||||
|
if (minutes > 0) return `${minutes}m ago`;
|
||||||
|
return 'just now';
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Auth module barrel export
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as AuthCommands } from './auth-commands';
|
||||||
|
export { default as ProfileRegistry } from './profile-registry';
|
||||||
|
export { default as ProfileDetector } from './profile-detector';
|
||||||
|
export type { ProfileDetectionResult, ProfileType, AllProfiles } from './profile-detector';
|
||||||
Reference in New Issue
Block a user