mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 20:17:45 +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.
|
||||
* 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.
|
||||
* Users login directly in each instance (no credential copying).
|
||||
*
|
||||
* Supports dual-mode configuration:
|
||||
* - Unified YAML format (config.yaml) when CCS_UNIFIED_CONFIG=1 or config.yaml exists
|
||||
* - Legacy JSON format (profiles.json) as fallback
|
||||
* Implementation Note: This is a facade that delegates to modular command handlers.
|
||||
* See ./commands/ for individual command implementations.
|
||||
*/
|
||||
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import ProfileRegistry from './profile-registry';
|
||||
import { ProfileMetadata } from '../types';
|
||||
import { InstanceManager } from '../management/instance-manager';
|
||||
import {
|
||||
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 { initUI, header, subheader, color, dim, warn, fail } from '../utils/ui';
|
||||
import packageJson from '../../package.json';
|
||||
import { isUnifiedMode } from '../config/unified-config-loader';
|
||||
import { exitWithError } from '../errors';
|
||||
import { ExitCode } from '../errors/exit-codes';
|
||||
|
||||
interface AuthCommandArgs {
|
||||
profileName?: string;
|
||||
force?: boolean;
|
||||
verbose?: boolean;
|
||||
json?: boolean;
|
||||
yes?: boolean;
|
||||
}
|
||||
|
||||
interface ProfileOutput {
|
||||
name: string;
|
||||
type: string;
|
||||
is_default: boolean;
|
||||
created: string;
|
||||
last_used: string | null;
|
||||
instance_path?: string;
|
||||
session_count?: number;
|
||||
}
|
||||
|
||||
interface ListOutput {
|
||||
version: string;
|
||||
profiles: ProfileOutput[];
|
||||
}
|
||||
// Import command handlers from modular structure
|
||||
import {
|
||||
type CommandContext,
|
||||
handleCreate,
|
||||
handleList,
|
||||
handleShow,
|
||||
handleRemove,
|
||||
handleDefault,
|
||||
handleResetDefault,
|
||||
} from './commands';
|
||||
|
||||
/**
|
||||
* Auth Commands Class
|
||||
* Auth Commands Class (Facade)
|
||||
*
|
||||
* Maintains class API for backward compatibility while delegating
|
||||
* to modular command handlers.
|
||||
*/
|
||||
class AuthCommands {
|
||||
private registry: ProfileRegistry;
|
||||
@@ -76,6 +43,17 @@ class AuthCommands {
|
||||
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
|
||||
*/
|
||||
@@ -138,513 +116,45 @@ class AuthCommands {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command arguments
|
||||
*/
|
||||
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
|
||||
* Create new profile - delegates to create-command.ts
|
||||
*/
|
||||
async handleCreate(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
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
|
||||
);
|
||||
}
|
||||
return handleCreate(this.getContext(), args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format relative time (e.g., "2h ago", "1d ago")
|
||||
*/
|
||||
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
|
||||
* List all profiles - delegates to list-command.ts
|
||||
*/
|
||||
async handleList(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
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);
|
||||
}
|
||||
return handleList(this.getContext(), args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show details for a specific profile
|
||||
* Show profile details - delegates to show-command.ts
|
||||
*/
|
||||
async handleShow(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
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);
|
||||
}
|
||||
return handleShow(this.getContext(), args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a saved profile
|
||||
* Remove profile - delegates to remove-command.ts
|
||||
*/
|
||||
async handleRemove(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
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
|
||||
);
|
||||
}
|
||||
return handleRemove(this.getContext(), args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default profile
|
||||
* Set default profile - delegates to default-command.ts
|
||||
*/
|
||||
async handleDefault(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
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);
|
||||
}
|
||||
return handleDefault(this.getContext(), args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset default profile (clear the custom default, restore original CCS behavior)
|
||||
* Reset default profile - delegates to default-command.ts
|
||||
*/
|
||||
async handleResetDefault(): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
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);
|
||||
}
|
||||
return handleResetDefault(this.getContext());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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