FEAT: Support CLAUDE_CONFIG_DIR (#99)

* chore: automatic change from linting

* feat: support CLAUDE_CONFIG_DIR on utilities

* feat: support CLAUDE_CONFIG_DIR from the main app UI

* fix: reduce duplication and handle installation using new constants

* feat: missed one place using hardcoded path

* chore: update CLAUDE.md

* docs: Update README
This commit is contained in:
Ramiro Rivera
2025-10-07 23:36:13 -04:00
committed by GitHub
parent 9feebf9a09
commit 9f26a071a7
6 changed files with 144 additions and 73 deletions
+4
View File
@@ -57,6 +57,10 @@ The project has dual runtime compatibility - works with both Bun and Node.js:
- Manages flex separator expansion
- **powerline.ts**: Powerline font detection and installation
- **claude-settings.ts**: Integration with Claude Code settings.json
- Respects `CLAUDE_CONFIG_DIR` environment variable with fallback to `~/.claude`
- Provides installation command constants (NPM, BUNX, self-managed)
- Detects installation status and manages settings.json updates
- Validates config directory paths with proper error handling
- **colors.ts**: Color definitions and ANSI code mapping
### Widgets (src/widgets/)
+18 -2
View File
@@ -125,6 +125,7 @@
- **🖥️ Interactive TUI** - Built-in configuration interface using React/Ink
- **⚙️ Global Options** - Apply consistent formatting across all widgets (padding, separators, bold, background)
- **🚀 Cross-platform** - Works seamlessly with both Bun and Node.js
- **🔧 Flexible Configuration** - Supports custom Claude Code config directory via `CLAUDE_CONFIG_DIR` environment variable
- **📏 Smart Width Detection** - Automatically adapts to terminal width with flex separators
- **⚡ Zero Config** - Sensible defaults that work out of the box
@@ -155,6 +156,15 @@ The interactive configuration tool provides a terminal UI where you can:
> 💡 **Tip:** Your settings are automatically saved to `~/.config/ccstatusline/settings.json`
> 🔧 **Custom Claude Config:** If your Claude Code configuration is in a non-standard location, set the `CLAUDE_CONFIG_DIR` environment variable:
> ```bash
> # Linux/macOS
> export CLAUDE_CONFIG_DIR=/custom/path/to/.claude
>
> # Windows PowerShell
> $env:CLAUDE_CONFIG_DIR="C:\custom\path\.claude"
> ```
---
## 🪟 Windows Support
@@ -294,7 +304,11 @@ For the best experience, configure Windows Terminal with these recommended setti
#### Claude Code Integration
Configure ccstatusline in your Claude Code settings:
**For Bun users** (Windows: `%USERPROFILE%\.claude\settings.json`):
**Settings Location:**
- Default: `~/.claude/settings.json` (Windows: `%USERPROFILE%\.claude\settings.json`)
- Custom: Set `CLAUDE_CONFIG_DIR` environment variable to use a different directory
**For Bun users**:
```json
{
"statusLine": "bunx ccstatusline@latest"
@@ -308,6 +322,8 @@ Configure ccstatusline in your Claude Code settings:
}
```
> 💡 **Custom Config Directory:** If you use a non-standard Claude Code configuration directory, set the `CLAUDE_CONFIG_DIR` environment variable before running ccstatusline. The tool will automatically detect and use your custom location.
### Performance on Windows
ccstatusline is optimized for Windows performance:
@@ -561,7 +577,7 @@ ccstatusline/
│ │ ├── renderer.ts # Core rendering logic
│ │ ├── powerline.ts # Powerline font utilities
│ │ ├── colors.ts # Color definitions
│ │ └── claude-settings.ts # Claude Code integration
│ │ └── claude-settings.ts # Claude Code integration (supports CLAUDE_CONFIG_DIR)
│ └── types/ # TypeScript type definitions
│ ├── Settings.ts
│ ├── Widget.ts
+41 -53
View File
@@ -8,6 +8,7 @@ import {
} from 'ink';
import Gradient from 'ink-gradient';
import React, {
useCallback,
useEffect,
useState
} from 'react';
@@ -15,6 +16,8 @@ import React, {
import type { Settings } from '../types/Settings';
import type { WidgetItem } from '../types/Widget';
import {
CCSTATUSLINE_COMMANDS,
getClaudeSettingsPath,
getExistingStatusLine,
installStatusLine,
isBunxAvailable,
@@ -129,6 +132,41 @@ export const App: React.FC = () => {
}
});
const handleInstallSelection = useCallback((command: string, displayName: string, useBunx: boolean) => {
void getExistingStatusLine().then((existing) => {
const isAlreadyInstalled = [CCSTATUSLINE_COMMANDS.NPM, CCSTATUSLINE_COMMANDS.BUNX, CCSTATUSLINE_COMMANDS.SELF_MANAGED].includes(existing ?? '');
let message: string;
if (existing && !isAlreadyInstalled) {
message = `This will modify ${getClaudeSettingsPath()}\n\nA status line is already configured: "${existing}"\nReplace it with ${command}?`;
} else if (isAlreadyInstalled) {
message = `ccstatusline is already installed in ${getClaudeSettingsPath()}\nUpdate it with ${command}?`;
} else {
message = `This will modify ${getClaudeSettingsPath()} to add ccstatusline with ${displayName}.\nContinue?`;
}
setConfirmDialog({
message,
action: async () => {
await installStatusLine(useBunx);
setIsClaudeInstalled(true);
setExistingStatusLine(command);
setScreen('main');
setConfirmDialog(null);
}
});
setScreen('confirm');
});
}, []);
const handleNpxInstall = useCallback(() => {
handleInstallSelection(CCSTATUSLINE_COMMANDS.NPM, 'npx', false);
}, [handleInstallSelection]);
const handleBunxInstall = useCallback(() => {
handleInstallSelection(CCSTATUSLINE_COMMANDS.BUNX, 'bunx', true);
}, [handleInstallSelection]);
if (!settings) {
return <Text>Loading settings...</Text>;
}
@@ -137,7 +175,7 @@ export const App: React.FC = () => {
if (isClaudeInstalled) {
// Uninstall
setConfirmDialog({
message: 'This will remove ccstatusline from ~/.claude/settings.json. Continue?',
message: `This will remove ccstatusline from ${getClaudeSettingsPath()}. Continue?`,
action: async () => {
await uninstallStatusLine();
setIsClaudeInstalled(false);
@@ -376,58 +414,8 @@ export const App: React.FC = () => {
<InstallMenu
bunxAvailable={isBunxAvailable()}
existingStatusLine={existingStatusLine}
onSelectNpx={() => {
void getExistingStatusLine().then((existing) => {
const isAlreadyInstalled = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'].includes(existing ?? '');
let message: string;
if (existing && !isAlreadyInstalled) {
message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with npx -y ccstatusline@latest?`;
} else if (isAlreadyInstalled) {
message = 'ccstatusline is already installed in ~/.claude/settings.json\nUpdate it with npx -y ccstatusline@latest?';
} else {
message = 'This will modify ~/.claude/settings.json to add ccstatusline with npx.\nContinue?';
}
setConfirmDialog({
message,
action: async () => {
await installStatusLine(false);
setIsClaudeInstalled(true);
setExistingStatusLine('npx -y ccstatusline@latest');
setScreen('main');
setConfirmDialog(null);
}
});
setScreen('confirm');
});
}}
onSelectBunx={() => {
void getExistingStatusLine().then((existing) => {
const isAlreadyInstalled = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'].includes(existing ?? '');
let message: string;
if (existing && !isAlreadyInstalled) {
message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with bunx -y ccstatusline@latest?`;
} else if (isAlreadyInstalled) {
message = 'ccstatusline is already installed in ~/.claude/settings.json\nUpdate it with bunx -y ccstatusline@latest?';
} else {
message = 'This will modify ~/.claude/settings.json to add ccstatusline with bunx.\nContinue?';
}
setConfirmDialog({
message,
action: async () => {
await installStatusLine(true);
setIsClaudeInstalled(true);
setExistingStatusLine('bunx -y ccstatusline@latest');
setScreen('main');
setConfirmDialog(null);
}
});
setScreen('confirm');
});
}}
onSelectNpx={handleNpxInstall}
onSelectBunx={handleBunxInstall}
onCancel={() => {
setScreen('main');
}}
+5 -1
View File
@@ -5,6 +5,8 @@ import {
} from 'ink';
import React, { useState } from 'react';
import { getClaudeSettingsPath } from '../../utils/claude-settings';
export interface InstallMenuProps {
bunxAvailable: boolean;
existingStatusLine: string | null;
@@ -95,7 +97,9 @@ export const InstallMenu: React.FC<InstallMenuProps> = ({
<Box marginTop={2}>
<Text dimColor>
The selected command will be written to ~/.claude/settings.json
The selected command will be written to
{' '}
{getClaudeSettingsPath()}
</Text>
</Box>
+7 -7
View File
@@ -73,10 +73,10 @@ const LineSelector: React.FC<LineSelectorProps> = ({
const powerlineEnabled = settings ? settings.powerline.enabled : false;
const powerlineTheme = settings ? settings.powerline.theme : undefined;
const isThemeManaged
= blockIfPowerlineActive
&& powerlineEnabled
&& powerlineTheme
&& powerlineTheme !== 'custom';
= blockIfPowerlineActive
&& powerlineEnabled
&& powerlineTheme
&& powerlineTheme !== 'custom';
// Handle keyboard input
useInput((input, key) => {
@@ -153,9 +153,9 @@ const LineSelector: React.FC<LineSelectorProps> = ({
if (showDeleteDialog && selectedLine) {
const suffix
= selectedLine.length > 0
? pluralize('widget', selectedLine.length, true)
: 'empty';
= selectedLine.length > 0
? pluralize('widget', selectedLine.length, true)
: 'empty';
return (
<Box flexDirection='column'>
+69 -10
View File
@@ -13,32 +13,89 @@ const readFile = fs.promises.readFile;
const writeFile = fs.promises.writeFile;
const mkdir = fs.promises.mkdir;
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
export const CCSTATUSLINE_COMMANDS = {
NPM: 'npx -y ccstatusline@latest',
BUNX: 'bunx -y ccstatusline@latest',
SELF_MANAGED: 'ccstatusline'
};
/**
* Determines the Claude config directory, checking CLAUDE_CONFIG_DIR environment variable first,
* then falling back to the default ~/.claude directory.
*/
export function getClaudeConfigDir(): string {
const envConfigDir = process.env.CLAUDE_CONFIG_DIR;
if (envConfigDir) {
try {
// Validate that the path is absolute and reasonable
const resolvedPath = path.resolve(envConfigDir);
// Check if directory exists or can be created
if (fs.existsSync(resolvedPath)) {
const stats = fs.statSync(resolvedPath);
if (stats.isDirectory()) {
return resolvedPath;
}
} else {
// Directory doesn't exist yet, but we can try to use it
// (mkdir will be called later when saving)
return resolvedPath;
}
} catch {
// Fall through to default on any error
}
}
// Default fallback
return path.join(os.homedir(), '.claude');
}
/**
* Gets the full path to the Claude settings.json file.
*/
export function getClaudeSettingsPath(): string {
return path.join(getClaudeConfigDir(), 'settings.json');
}
export async function loadClaudeSettings(): Promise<ClaudeSettings> {
try {
if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
const settingsPath = getClaudeSettingsPath();
if (!fs.existsSync(settingsPath)) {
return {};
}
const content = await readFile(CLAUDE_SETTINGS_PATH, 'utf-8');
const content = await readFile(settingsPath, 'utf-8');
return JSON.parse(content) as ClaudeSettings;
} catch {
return {};
}
}
export async function saveClaudeSettings(settings: ClaudeSettings): Promise<void> {
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
export async function saveClaudeSettings(
settings: ClaudeSettings
): Promise<void> {
const settingsPath = getClaudeSettingsPath();
const dir = path.dirname(settingsPath);
await mkdir(dir, { recursive: true });
await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
await writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
}
export async function isInstalled(): Promise<boolean> {
const settings = await loadClaudeSettings();
// Check if command is either npx or bunx version AND padding is 0 (or undefined for new installs)
const validCommands = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'];
return validCommands.includes(settings.statusLine?.command ?? '')
&& (settings.statusLine?.padding === 0 || settings.statusLine?.padding === undefined);
const validCommands = [
// Default autoinstalled npm command
CCSTATUSLINE_COMMANDS.NPM,
// Default autoinstalled bunx command
CCSTATUSLINE_COMMANDS.BUNX,
// Self managed installation command
CCSTATUSLINE_COMMANDS.SELF_MANAGED
];
return (
validCommands.includes(settings.statusLine?.command ?? '')
&& (settings.statusLine?.padding === 0
|| settings.statusLine?.padding === undefined)
);
}
export function isBunxAvailable(): boolean {
@@ -58,7 +115,9 @@ export async function installStatusLine(useBunx = false): Promise<void> {
// Update settings with our status line (confirmation already handled in TUI)
settings.statusLine = {
type: 'command',
command: useBunx ? 'bunx -y ccstatusline@latest' : 'npx -y ccstatusline@latest',
command: useBunx
? CCSTATUSLINE_COMMANDS.BUNX
: CCSTATUSLINE_COMMANDS.NPM,
padding: 0
};