FABRIC/src/config.ts
jedarden 5e029c142c
Some checks are pending
CI / test (18.x) (push) Waiting to run
CI / test (20.x) (push) Waiting to run
CI / test (22.x) (push) Waiting to run
feat(bf-3xp): add bead workspace scanner + project breakdown in /api/productivity
Phase 9 implementation: Bead workspace scanner and project breakdown.

- Add beadWorkspaceScanner.ts to scan .beads/issues.jsonl files
- Count CLOSED beads per project, deriving project from bead id prefix
- Use close_reason/closed_at/assignee for productivity tracking
- Add configurable workspace list in config.ts (WorkspaceConfig interface)
- Extend GET /api/productivity to add byProject array
- Add By Project section to ProductivityPanel React component
- Add tests for bead workspace scanner

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:34:20 -04:00

440 lines
12 KiB
TypeScript

/**
* 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'),
workspaces: path.join(CONFIG_DIR, 'workspaces.json'),
};
/**
* Workspace configuration for bead scanning
*/
export interface WorkspaceConfig {
/** Workspace path (e.g., /home/coding/FABRIC) */
path: string;
/** Project name derived from path or override */
name: string;
/** Bead ID prefix for this workspace (e.g., 'bf' for global, 'kt' for kalshi-tape) */
prefix: string;
}
/**
* Load workspace configurations
*/
export function loadWorkspaces(): WorkspaceConfig[] {
try {
if (fs.existsSync(CONFIG_FILES.workspaces)) {
const data = fs.readFileSync(CONFIG_FILES.workspaces, 'utf-8');
const config = JSON.parse(data) as { workspaces: WorkspaceConfig[] };
return config.workspaces || [];
}
} catch {
// Ignore errors
}
// Default workspace list
return getDefaultWorkspaces();
}
/**
* Get default workspace list by scanning /home/coding for .beads directories
*/
function getDefaultWorkspaces(): WorkspaceConfig[] {
const workspaces: WorkspaceConfig[] = [];
const codingDir = path.join(HOME);
try {
if (!fs.existsSync(codingDir)) {
return workspaces;
}
const entries = fs.readdirSync(codingDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const projectPath = path.join(codingDir, entry.name);
const beadsDir = path.join(projectPath, '.beads');
const issuesFile = path.join(beadsDir, 'issues.jsonl');
if (fs.existsSync(issuesFile)) {
// Extract project name from directory name
const name = entry.name;
// Determine prefix from bead ID format in the file
const prefix = detectBeadPrefix(issuesFile) || 'bf';
workspaces.push({
path: projectPath,
name,
prefix,
});
}
}
} catch {
// Ignore errors
}
return workspaces;
}
/**
* Detect bead ID prefix by reading the first line of issues.jsonl
*/
function detectBeadPrefix(issuesFile: string): string | null {
try {
const content = fs.readFileSync(issuesFile, 'utf-8');
const firstLine = content.split('\n')[0];
if (!firstLine) return null;
const match = firstLine.match(/"id":"([a-z]+)-/);
return match ? match[1] : null;
} catch {
return null;
}
}
/**
* 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('');
// Workspaces
console.log('Workspaces:');
const workspaces = loadWorkspaces();
if (workspaces.length === 0) {
console.log(' No workspaces configured');
} else {
console.log(` Count: ${workspaces.length}`);
workspaces.forEach(w => {
console.log(` - ${w.name} (${w.prefix}-*)`);
console.log(` Path: ${w.path}`);
});
}
console.log(` File: ${CONFIG_FILES.workspaces}`);
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;
}