@invisiblecities/sidequest-cqo
Version:
Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection
319 lines (313 loc) ⢠11.2 kB
JavaScript
/**
* @fileoverview User Preferences Manager - Configuration-Agnostic User Settings
*
* Manages persistent user preferences and educational prompts for the Code Quality Orchestrator.
* This system respects user choices while providing gentle guidance toward best practices.
*
* Key Features:
* - Singleton pattern for consistent preferences across sessions
* - Schema migration for forward compatibility
* - Educational warnings (TypeScript/ESLint separation) with user control
* - Configuration-agnostic defaults that don't impose opinions
* - Project-scoped or global preference storage options
*
* Architecture:
* The preferences system operates on three levels:
* 1. **Default Preferences**: Conservative, non-opinionated defaults
* 2. **User Overrides**: Saved choices that persist across sessions
* 3. **Runtime Configuration**: CLI flags that override preferences
*
* Educational Philosophy:
* - Suggest best practices but never force them
* - One-time educational prompts with "don't show again" options
* - Respect user expertise level and preference for guidance
*
* @example Basic Usage
* ```typescript
* const prefs = PreferencesManager.getInstance('./project-data');
*
* // Check if user wants separation guidance
* if (prefs.shouldShowTscEslintWarning()) {
* const choice = await prefs.showTscEslintSeparationWarning();
* console.log('User chose:', choice);
* }
*
* // Get user's analysis preferences
* const mode = prefs.getAnalysisMode(); // 'errors-only' | 'warnings-and-errors' | 'all'
* const colors = prefs.getColorScheme(); // 'auto' | 'light' | 'dark'
* ```
*
* @example Configuration Management
* ```typescript
* // Update specific preference section
* prefs.updatePreference('display', {
* colorScheme: 'dark',
* verboseOutput: true
* });
*
* // Reset everything to defaults
* prefs.resetToDefaults();
*
* // Get full config for debugging
* const allPrefs = prefs.getAllPreferences();
* ```
*
* @author SideQuest
* @version 1.0.0
*/
import * as fs from "node:fs";
import path from "node:path";
import * as os from "node:os";
export class PreferencesManager {
static instance;
preferencesPath;
preferences;
constructor(dataDirectory) {
// Store preferences in user's data directory or project-specific location
const baseDirectory = dataDirectory || path.join(os.homedir(), ".sidequest-cqo");
this.preferencesPath = path.join(baseDirectory, "user-preferences.json");
// Ensure directory exists
if (!fs.existsSync(baseDirectory)) {
fs.mkdirSync(baseDirectory, { recursive: true });
}
this.preferences = this.loadPreferences();
}
static getInstance(dataDirectory) {
if (!PreferencesManager.instance) {
PreferencesManager.instance = new PreferencesManager(dataDirectory);
}
return PreferencesManager.instance;
}
/**
* Load preferences from file or create defaults
*/
loadPreferences() {
try {
if (fs.existsSync(this.preferencesPath)) {
const content = fs.readFileSync(this.preferencesPath, "utf8");
const loaded = JSON.parse(content);
// Validate and migrate if needed
return this.migratePreferences(loaded);
}
}
catch (error) {
console.warn(`[Preferences] Could not load preferences: ${error}`);
}
// Return defaults
return this.getDefaultPreferences();
}
/**
* Get default preferences
*/
getDefaultPreferences() {
return {
schemaVersion: "1.0.0",
preferences: {
analysis: {
defaultMode: "errors-only", // Conservative default
strictMode: false,
includePatternChecking: false,
},
warnings: {
showTscEslintSeparationWarning: true, // Important best practice
showPerformanceWarnings: true,
showConfigurationHints: true,
},
display: {
colorScheme: "auto",
verboseOutput: false,
showProgressIndicators: true,
},
watch: {
autoDetectConfigChanges: true,
debounceMs: 500,
intervalMs: 3000,
},
customTypeScriptScripts: {
enabled: true,
defaultPreset: "safe",
presetMappings: {
safe: ["tsc:safe", "type-check", "tsc:dev"],
strict: ["tsc:strict", "type-check:strict", "tsc:ci"],
dev: ["tsc:dev", "tsc:safe", "type-check"],
ci: ["tsc:ci", "tsc:strict", "type-check:strict"],
},
scriptTimeout: 60_000,
failureHandling: "warn",
},
customESLintScripts: {
enabled: true,
defaultPreset: "safe",
presetMappings: {
safe: ["lint:check", "eslint", "lint"],
fix: ["lint:fix", "eslint:fix"],
strict: ["lint:strict", "eslint:strict"],
ci: ["lint:ci", "eslint:ci"],
},
scriptTimeout: 60_000,
failureHandling: "warn",
},
},
userChoices: {
hasSeenTscEslintWarning: false,
hasConfiguredSeparationOfConcerns: false,
hasCompletedFirstRun: false,
preferredEngine: "typescript-only", // Safe default
lastConfigUpdate: undefined,
},
};
}
/**
* Migrate preferences between schema versions
*/
migratePreferences(loaded) {
// For now, just ensure all required fields exist
const defaults = this.getDefaultPreferences();
return {
...defaults,
...loaded,
preferences: {
...defaults.preferences,
...loaded.preferences,
analysis: {
...defaults.preferences.analysis,
...loaded.preferences?.analysis,
},
warnings: {
...defaults.preferences.warnings,
...loaded.preferences?.warnings,
},
display: {
...defaults.preferences.display,
...loaded.preferences?.display,
},
watch: {
...defaults.preferences.watch,
...loaded.preferences?.watch,
},
customTypeScriptScripts: {
...defaults.preferences.customTypeScriptScripts,
...loaded.preferences?.customTypeScriptScripts,
},
customESLintScripts: {
...defaults.preferences.customESLintScripts,
...loaded.preferences?.customESLintScripts,
},
},
userChoices: {
...defaults.userChoices,
...loaded.userChoices,
},
};
}
/**
* Save preferences to file
*/
savePreferences() {
try {
this.preferences.userChoices.lastConfigUpdate = new Date().toISOString();
const content = JSON.stringify(this.preferences, undefined, 2);
fs.writeFileSync(this.preferencesPath, content, "utf8");
}
catch (error) {
console.warn(`[Preferences] Could not save preferences: ${error}`);
}
}
/**
* Check if user should see TypeScript/ESLint separation warning
*/
shouldShowTscEslintWarning() {
return (this.preferences.preferences.warnings.showTscEslintSeparationWarning &&
!this.preferences.userChoices.hasSeenTscEslintWarning);
}
/**
* Show TypeScript/ESLint separation warning and get user choice
*/
showTscEslintSeparationWarning() {
console.log(`
š§ Configuration Best Practice Recommendation
We detected that you're using both TypeScript compilation checking and ESLint.
For optimal performance and clarity, we recommend:
ā
TypeScript: Type safety, compilation errors (tsc --noEmit)
ā
ESLint: Code style, best practices, custom rules
This separation avoids:
ā Duplicate rule execution
ā Conflicting error messages
ā Performance overhead
ā Configuration complexity
How would you like to proceed?
1) TypeScript only (recommended for type safety focus)
2) Both tools with separation of concerns (recommended for full analysis)
3) Both tools mixed (current setup, may have overlap)
4) Don't show this warning again
Your choice will be saved and can be changed in preferences.
`);
// In a real implementation, this would use a proper prompt library
// For now, we'll simulate the choice
const choice = "both-separate"; // Default safe choice
this.preferences.userChoices.hasSeenTscEslintWarning = true;
this.preferences.userChoices.preferredEngine =
choice === "disable-warning" ? "both-mixed" : choice;
if (choice === "disable-warning") {
this.preferences.preferences.warnings.showTscEslintSeparationWarning = false;
}
this.savePreferences();
return Promise.resolve(choice);
}
/**
* Get user's preferred analysis mode
*/
getAnalysisMode() {
return this.preferences.preferences.analysis.defaultMode;
}
/**
* Get user's strict mode preference
*/
getStrictMode() {
return this.preferences.preferences.analysis.strictMode;
}
/**
* Get user's color scheme preference
*/
getColorScheme() {
return this.preferences.preferences.display.colorScheme;
}
/**
* Update user preference
*/
updatePreference(section, updates) {
this.preferences.preferences[section] = {
...this.preferences.preferences[section],
...updates,
};
this.savePreferences();
}
/**
* Get all preferences (for debugging/config display)
*/
getAllPreferences() {
return { ...this.preferences };
}
/**
* Reset preferences to defaults
*/
resetToDefaults() {
this.preferences = this.getDefaultPreferences();
this.savePreferences();
}
/**
* Update user choice/state
*/
updateUserChoice(key, value) {
this.preferences.userChoices[key] = value;
this.savePreferences();
}
/**
* Check if user has configured separation of concerns
*/
hasSeparationOfConcerns() {
return (this.preferences.userChoices.hasConfiguredSeparationOfConcerns ||
this.preferences.userChoices.preferredEngine === "both-separate");
}
}
//# sourceMappingURL=preferences-manager.js.map