feat: add interactive CLI token configuration with settings persistence

Implements comprehensive token management with intelligent priority resolution.
Token sources (highest to lowest priority): environment variable, settings file,
interactive prompt. Changes include:

- Updated home directory from ~/.happy to ~/.config/hapi (env var: HAPI_HOME_DIR)
- Added cliApiToken field to Settings interface for persistent storage
- Made cliApiToken mutable in Configuration class (private field with getter/setter)
- Created ui/tokenInit.ts module for token initialization with fallback prompt
- Enhanced auth command: login (interactive input), logout (clear token), status (show source)
- Updated doctor command to correctly identify token source and mask saved token in output
- Token initialization called early in startup flow to ensure availability before API use
This commit is contained in:
weishu
2025-12-16 21:54:55 +08:00
parent 0485065b87
commit c2b886896b
6 changed files with 151 additions and 29 deletions
+15 -6
View File
@@ -23,7 +23,7 @@ import packageJson from '../../package.json'
export function getEnvironmentInfo(): Record<string, any> {
return {
PWD: process.env.PWD,
HAPPY_HOME_DIR: process.env.HAPPY_HOME_DIR,
HAPI_HOME_DIR: process.env.HAPI_HOME_DIR,
HAPPY_BOT_URL: process.env.HAPPY_BOT_URL,
HAPPY_PROJECT_ROOT: process.env.HAPPY_PROJECT_ROOT,
CLI_API_TOKEN_SET: Boolean(process.env.CLI_API_TOKEN),
@@ -111,7 +111,7 @@ export async function runDoctorCommand(filter?: 'all' | 'daemon'): Promise<void>
// Environment
console.log(chalk.bold('\n🌍 Environment Variables'));
const env = getEnvironmentInfo();
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray('not set')}`);
console.log(`HAPI_HOME_DIR: ${env.HAPI_HOME_DIR ? chalk.green(env.HAPI_HOME_DIR) : chalk.gray('not set')}`);
console.log(`HAPPY_BOT_URL: ${env.HAPPY_BOT_URL ? chalk.green(env.HAPPY_BOT_URL) : chalk.gray('not set')}`);
console.log(`CLI_API_TOKEN: ${env.CLI_API_TOKEN_SET ? chalk.green('set') : chalk.gray('not set')}`);
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow('ENABLED') : chalk.gray('not set')}`);
@@ -119,21 +119,30 @@ export async function runDoctorCommand(filter?: 'all' | 'daemon'): Promise<void>
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray('not set')}`);
// Settings
let settings;
try {
const settings = await readSettings();
settings = await readSettings();
console.log(chalk.bold('\n📄 Settings (settings.json):'));
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
// Hide cliApiToken in output for security
const displaySettings = { ...settings, cliApiToken: settings.cliApiToken ? '***' : undefined };
console.log(chalk.gray(JSON.stringify(displaySettings, null, 2)));
} catch (error) {
console.log(chalk.bold('\n📄 Settings:'));
console.log(chalk.red('❌ Failed to read settings'));
settings = {};
}
// Authentication status (direct-connect)
console.log(chalk.bold('\n🔐 Direct Connect Auth'));
if (configuration.cliApiToken) {
console.log(chalk.green('✓ CLI_API_TOKEN is set'));
const envToken = process.env.CLI_API_TOKEN;
const settingsToken = settings.cliApiToken;
const hasToken = Boolean(envToken || settingsToken);
const tokenSource = envToken ? 'environment variable' : (settingsToken ? 'settings file' : 'none');
if (hasToken) {
console.log(chalk.green(`✓ CLI_API_TOKEN is set (from ${tokenSource})`));
} else {
console.log(chalk.red('❌ CLI_API_TOKEN is not set'));
console.log(chalk.gray(' Run `hapi auth login` to configure or set CLI_API_TOKEN env var'));
}
// Legacy credentials (unused in direct-connect mode)
+65
View File
@@ -0,0 +1,65 @@
/**
* Token initialization module
*
* Handles CLI_API_TOKEN initialization with priority:
* 1. Environment variable (highest - allows temporary override)
* 2. Settings file (~/.config/hapi/settings.json)
* 3. Interactive prompt (only when both above are missing)
*/
import * as readline from 'node:readline/promises'
import { stdin as input, stdout as output } from 'node:process'
import chalk from 'chalk'
import { configuration } from '@/configuration'
import { readSettings, updateSettings } from '@/persistence'
/**
* Initialize CLI API token
* Must be called before any API operations
*/
export async function initializeToken(): Promise<void> {
// 1. Environment variable has highest priority (allows temporary override)
if (configuration.cliApiToken) {
return
}
// 2. Read from settings file
const settings = await readSettings()
if (settings.cliApiToken) {
configuration._setCliApiToken(settings.cliApiToken)
return
}
// 3. Non-TTY environment cannot prompt, fail with clear error
if (!process.stdin.isTTY) {
throw new Error('CLI_API_TOKEN is required. Set it via environment variable or run `hapi auth login`.')
}
// 4. Interactive prompt
const token = await promptForToken()
// 5. Save and update configuration
await updateSettings(current => ({
...current,
cliApiToken: token
}))
configuration._setCliApiToken(token)
}
async function promptForToken(): Promise<string> {
const rl = readline.createInterface({ input, output })
console.log(chalk.yellow('\nNo CLI_API_TOKEN found.'))
console.log(chalk.gray('You can set it via environment variable or enter it now.\n'))
try {
const token = await rl.question(chalk.cyan('Enter CLI_API_TOKEN: '))
if (!token.trim()) {
throw new Error('Token cannot be empty')
}
console.log(chalk.green('\nToken saved to ~/.config/hapi/settings.json'))
return token.trim()
} finally {
rl.close()
}
}