feat(cli): add fabric config command for configuration management
Add a new `fabric config` command that provides a user-friendly interface to manage FABRIC configuration without manual file editing. Features: - `fabric config` - Show current configuration (theme, presets, recent commands, filter state) - `fabric config theme [theme]` - Show or set theme (dark/light) - `fabric config presets list` - List all focus presets - `fabric config presets delete <name>` - Delete a focus preset - `fabric config clear` - Clear configuration state (with --theme, --presets, --commands, --filters, --all options) Config files managed: - ~/.fabric/theme.json (theme preference) - ~/.fabric/focus-presets.json (focus mode presets) - ~/.fabric/recent-commands.json (command history) - ~/.fabric-filter-state.json (filter persistence) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
08fdca5810
commit
982f93162c
2 changed files with 341 additions and 0 deletions
|
|
@ -17,6 +17,7 @@ import { formatEvent } from './parser.js';
|
|||
import { getStore } from './store.js';
|
||||
import { createWebServer } from './web/index.js';
|
||||
import { EventDeduplicator } from './normalizer.js';
|
||||
import { createConfigCommand } from './config.js';
|
||||
import * as fs from 'fs';
|
||||
import * as net from 'net';
|
||||
import type { LogLevel, EventFilter, LogEvent } from './types.js';
|
||||
|
|
@ -635,4 +636,7 @@ program
|
|||
}
|
||||
});
|
||||
|
||||
// Add config command
|
||||
program.addCommand(createConfigCommand());
|
||||
|
||||
program.parse();
|
||||
|
|
|
|||
337
src/config.ts
Normal file
337
src/config.ts
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
/**
|
||||
* FABRIC Config CLI Command
|
||||
*
|
||||
* Usage:
|
||||
* fabric config - Show current configuration
|
||||
* fabric config theme - Show/set theme
|
||||
* fabric config presets - Manage focus presets
|
||||
* fabric config clear - Clear all config state
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { Command } from 'commander';
|
||||
import { createTuiPresetManager } from './focusPresets.js';
|
||||
|
||||
const HOME = process.env.HOME || '';
|
||||
const CONFIG_DIR = path.join(HOME, '.fabric');
|
||||
|
||||
/**
|
||||
* Config file paths
|
||||
*/
|
||||
const CONFIG_FILES = {
|
||||
theme: path.join(CONFIG_DIR, 'theme.json'),
|
||||
presets: path.join(CONFIG_DIR, 'focus-presets.json'),
|
||||
recentCommands: path.join(CONFIG_DIR, 'recent-commands.json'),
|
||||
filterState: path.join(HOME, '.fabric-filter-state.json'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Show current configuration
|
||||
*/
|
||||
function showConfig(): void {
|
||||
console.log('FABRIC Configuration');
|
||||
console.log('===================');
|
||||
console.log(`Config directory: ${CONFIG_DIR}`);
|
||||
console.log('');
|
||||
|
||||
// Theme
|
||||
console.log('Theme:');
|
||||
const theme = loadTheme();
|
||||
console.log(` Current: ${theme}`);
|
||||
console.log(` File: ${CONFIG_FILES.theme}`);
|
||||
console.log('');
|
||||
|
||||
// Presets
|
||||
console.log('Focus Presets:');
|
||||
const presetManager = createTuiPresetManager();
|
||||
const presets = presetManager.getPresets();
|
||||
if (presets.length === 0) {
|
||||
console.log(' No presets saved');
|
||||
} else {
|
||||
console.log(` Count: ${presets.length}`);
|
||||
presets.forEach(p => {
|
||||
const createdAt = new Date(p.createdAt).toLocaleString();
|
||||
console.log(` - ${p.name}`);
|
||||
console.log(` Workers: ${p.pinnedWorkers.length}, Beads: ${p.pinnedBeads.length}`);
|
||||
console.log(` Created: ${createdAt}`);
|
||||
if (p.description) {
|
||||
console.log(` Description: ${p.description}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log(` File: ${CONFIG_FILES.presets}`);
|
||||
console.log('');
|
||||
|
||||
// Recent commands
|
||||
console.log('Recent Commands:');
|
||||
const recentCommands = loadRecentCommands();
|
||||
if (recentCommands.length === 0) {
|
||||
console.log(' No recent commands');
|
||||
} else {
|
||||
console.log(` Count: ${recentCommands.length}`);
|
||||
recentCommands.slice(0, 5).forEach((cmd, i) => {
|
||||
console.log(` ${i + 1}. ${cmd}`);
|
||||
});
|
||||
if (recentCommands.length > 5) {
|
||||
console.log(` ... and ${recentCommands.length - 5} more`);
|
||||
}
|
||||
}
|
||||
console.log(` File: ${CONFIG_FILES.recentCommands}`);
|
||||
console.log('');
|
||||
|
||||
// Filter state
|
||||
console.log('Filter State:');
|
||||
const filterState = loadFilterState();
|
||||
if (!filterState) {
|
||||
console.log(' No saved filter state');
|
||||
} else {
|
||||
console.log(' Saved filters:');
|
||||
for (const [key, value] of Object.entries(filterState)) {
|
||||
console.log(` ${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
console.log(` File: ${CONFIG_FILES.filterState}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current theme from config
|
||||
*/
|
||||
function loadTheme(): string {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILES.theme)) {
|
||||
const content = fs.readFileSync(CONFIG_FILES.theme, 'utf-8');
|
||||
const config = JSON.parse(content);
|
||||
return config.theme || 'dark';
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme in config
|
||||
*/
|
||||
function setTheme(theme: string): void {
|
||||
if (theme !== 'dark' && theme !== 'light') {
|
||||
console.error(`Invalid theme: ${theme}. Must be 'dark' or 'light'.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure config directory exists
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const config = { theme };
|
||||
fs.writeFileSync(CONFIG_FILES.theme, JSON.stringify(config, null, 2), 'utf-8');
|
||||
console.log(`Theme set to: ${theme}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to set theme: ${(err as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load recent commands from config
|
||||
*/
|
||||
function loadRecentCommands(): string[] {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILES.recentCommands)) {
|
||||
const data = fs.readFileSync(CONFIG_FILES.recentCommands, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load filter state from config
|
||||
*/
|
||||
function loadFilterState(): Record<string, unknown> | null {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILES.filterState)) {
|
||||
const data = fs.readFileSync(CONFIG_FILES.filterState, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List focus presets
|
||||
*/
|
||||
function listPresets(): void {
|
||||
const presetManager = createTuiPresetManager();
|
||||
const presets = presetManager.getPresets();
|
||||
|
||||
console.log('Focus Presets');
|
||||
console.log('=============');
|
||||
|
||||
if (presets.length === 0) {
|
||||
console.log('No presets saved.');
|
||||
console.log('');
|
||||
console.log('Create a preset from the TUI by:');
|
||||
console.log(' 1. Pin workers and beads in focus mode');
|
||||
console.log(' 2. Open command palette (Ctrl+P)');
|
||||
console.log(' 3. Select "Save current view as preset"');
|
||||
return;
|
||||
}
|
||||
|
||||
presets.forEach(preset => {
|
||||
const createdAt = new Date(preset.createdAt).toLocaleString();
|
||||
console.log('');
|
||||
console.log(`Name: ${preset.name}`);
|
||||
console.log(` Workers: ${preset.pinnedWorkers.length > 0 ? preset.pinnedWorkers.join(', ') : 'none'}`);
|
||||
console.log(` Beads: ${preset.pinnedBeads.length > 0 ? preset.pinnedBeads.join(', ') : 'none'}`);
|
||||
console.log(` Created: ${createdAt}`);
|
||||
if (preset.description) {
|
||||
console.log(` Description: ${preset.description}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a focus preset
|
||||
*/
|
||||
function deletePreset(name: string): void {
|
||||
const presetManager = createTuiPresetManager();
|
||||
|
||||
if (!presetManager.hasPreset(name)) {
|
||||
console.error(`Preset not found: ${name}`);
|
||||
console.log('');
|
||||
console.log('Available presets:');
|
||||
const presets = presetManager.getPresetNames();
|
||||
if (presets.length === 0) {
|
||||
console.log(' (none)');
|
||||
} else {
|
||||
presets.forEach(p => console.log(` - ${p}`));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (presetManager.deletePreset(name)) {
|
||||
console.log(`Deleted preset: ${name}`);
|
||||
} else {
|
||||
console.error(`Failed to delete preset: ${name}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all config state
|
||||
*/
|
||||
function clearConfig(options: { theme?: boolean; presets?: boolean; commands?: boolean; filters?: boolean; all?: boolean }): void {
|
||||
const filesToDelete: string[] = [];
|
||||
|
||||
if (options.all || options.theme) {
|
||||
filesToDelete.push(CONFIG_FILES.theme);
|
||||
}
|
||||
if (options.all || options.presets) {
|
||||
filesToDelete.push(CONFIG_FILES.presets);
|
||||
}
|
||||
if (options.all || options.commands) {
|
||||
filesToDelete.push(CONFIG_FILES.recentCommands);
|
||||
}
|
||||
if (options.all || options.filters) {
|
||||
filesToDelete.push(CONFIG_FILES.filterState);
|
||||
}
|
||||
|
||||
if (filesToDelete.length === 0) {
|
||||
console.log('Nothing to clear. Use --all or specify what to clear.');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --theme Clear theme preference');
|
||||
console.log(' --presets Clear focus presets');
|
||||
console.log(' --commands Clear recent commands');
|
||||
console.log(' --filters Clear filter state');
|
||||
console.log(' --all Clear all config');
|
||||
return;
|
||||
}
|
||||
|
||||
let deletedCount = 0;
|
||||
for (const file of filesToDelete) {
|
||||
try {
|
||||
if (fs.existsSync(file)) {
|
||||
fs.unlinkSync(file);
|
||||
console.log(`Deleted: ${file}`);
|
||||
deletedCount++;
|
||||
} else {
|
||||
console.log(`Not found (skipped): ${file}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete ${file}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount === 0) {
|
||||
console.log('');
|
||||
console.log('No files were deleted.');
|
||||
} else {
|
||||
console.log('');
|
||||
console.log(`Deleted ${deletedCount} config file(s).`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the config command
|
||||
*/
|
||||
export function createConfigCommand(): Command {
|
||||
const cmd = new Command('config')
|
||||
.description('Manage FABRIC configuration')
|
||||
.action(() => {
|
||||
showConfig();
|
||||
});
|
||||
|
||||
// Theme subcommand
|
||||
cmd.command('theme')
|
||||
.description('Show or set theme')
|
||||
.argument('[theme]', 'Theme to set (dark or light)')
|
||||
.action((theme?: string) => {
|
||||
if (theme) {
|
||||
setTheme(theme);
|
||||
} else {
|
||||
console.log(`Current theme: ${loadTheme()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Presets subcommand
|
||||
const presetsCmd = cmd.command('presets')
|
||||
.description('Manage focus presets');
|
||||
|
||||
presetsCmd.command('list')
|
||||
.alias('ls')
|
||||
.description('List all focus presets')
|
||||
.action(() => {
|
||||
listPresets();
|
||||
});
|
||||
|
||||
presetsCmd.command('delete')
|
||||
.alias('rm')
|
||||
.description('Delete a focus preset')
|
||||
.argument('<name>', 'Preset name to delete')
|
||||
.action((name: string) => {
|
||||
deletePreset(name);
|
||||
});
|
||||
|
||||
// Clear subcommand
|
||||
cmd.command('clear')
|
||||
.description('Clear configuration state')
|
||||
.option('--theme', 'Clear theme preference')
|
||||
.option('--presets', 'Clear focus presets')
|
||||
.option('--commands', 'Clear recent commands')
|
||||
.option('--filters', 'Clear filter state')
|
||||
.option('--all', 'Clear all config')
|
||||
.action((options) => {
|
||||
clearConfig(options);
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue