supernal-coding
Version:
Comprehensive development workflow CLI with kanban task management, project validation, git safety hooks, and cross-project distribution system
379 lines (330 loc) • 12.1 kB
JavaScript
// local-state-manager.js - Manage local .sc folder and state tracking
// Part of REQ-053: Enhanced Local Repository Sync Check with State Management
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const { execSync } = require('child_process');
class LocalStateManager {
constructor(options = {}) {
this.projectRoot = options.projectRoot || process.cwd();
this.verbose = options.verbose || false;
this.scFolderPath = path.join(this.projectRoot, '.supernalcoding');
this.stateFilePath = path.join(this.scFolderPath, 'state.json');
this.versionFilePath = path.join(this.scFolderPath, 'last-sync-version.txt');
// Config is now handled by the main config system, not local files
this.configManager = options.configManager || null;
}
/**
* Initialize .supernalcoding folder structure and gitignore management
*/
async initializeScFolder() {
try {
await fs.ensureDir(this.scFolderPath);
// Create initial state if it doesn't exist
if (!await fs.pathExists(this.stateFilePath)) {
const initialState = {
initialized: new Date().toISOString(),
lastSyncCheck: null,
lastSyncVersion: null,
syncHistory: [],
repoType: await this.detectRepoType()
};
await fs.writeJson(this.stateFilePath, initialState, { spaces: 2 });
}
// Ensure .gitignore excludes the entire .supernalcoding folder
await this.ensureGitIgnore();
if (this.verbose) {
console.log(chalk.green(`✅ .supernalcoding folder initialized at ${this.scFolderPath}`));
}
return true;
} catch (error) {
console.error(chalk.red(`❌ Failed to initialize .supernalcoding folder: ${error.message}`));
return false;
}
}
/**
* Ensure .gitignore properly excludes .supernalcoding folder
*/
async ensureGitIgnore() {
try {
const gitignorePath = path.join(this.projectRoot, '.gitignore');
const ignoreEntry = '.supernalcoding/';
// Read existing .gitignore or create empty content
let gitignoreContent = '';
if (await fs.pathExists(gitignorePath)) {
gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
}
// Check if .supernalcoding is already ignored
if (!gitignoreContent.includes('.supernalcoding')) {
// Add .supernalcoding to .gitignore
const newContent = gitignoreContent.trim() + '\n\n# Supernal Coding local state (auto-managed)\n.supernalcoding/\n';
await fs.writeFile(gitignorePath, newContent);
if (this.verbose) {
console.log(chalk.green('✅ Added .supernalcoding/ to .gitignore'));
}
}
} catch (error) {
if (this.verbose) {
console.error(chalk.yellow(`⚠️ Could not update .gitignore: ${error.message}`));
}
}
}
/**
* Detect what type of repository this is
*/
async detectRepoType() {
const indicators = [
{ file: 'supernal-code-package/package.json', type: 'supernal-source' },
{ file: 'package.json', type: 'npm-project' },
{ file: '.git', type: 'git-repo' },
{ file: 'node_modules', type: 'node-project' }
];
for (const indicator of indicators) {
if (await fs.pathExists(path.join(this.projectRoot, indicator.file))) {
return indicator.type;
}
}
return 'unknown';
}
/**
* Get current state
*/
async getState() {
try {
if (!await fs.pathExists(this.stateFilePath)) {
await this.initializeScFolder();
}
return await fs.readJson(this.stateFilePath);
} catch (error) {
if (this.verbose) {
console.error(chalk.yellow(`⚠️ Could not read state: ${error.message}`));
}
return null;
}
}
/**
* Update state
*/
async updateState(updates) {
try {
const currentState = await this.getState() || {};
const newState = { ...currentState, ...updates, lastUpdated: new Date().toISOString() };
await fs.writeJson(this.stateFilePath, newState, { spaces: 2 });
return newState;
} catch (error) {
console.error(chalk.red(`❌ Failed to update state: ${error.message}`));
return null;
}
}
/**
* Record a sync check
*/
async recordSyncCheck(result) {
try {
const state = await this.getState();
if (!state) return false;
const syncRecord = {
timestamp: new Date().toISOString(),
globalVersion: result.globalVersion,
localVersion: result.localVersion,
isInSync: result.isInSync,
issues: result.issues || []
};
// Keep last 10 sync checks
const syncHistory = state.syncHistory || [];
syncHistory.unshift(syncRecord);
if (syncHistory.length > 10) {
syncHistory.splice(10);
}
await this.updateState({
lastSyncCheck: syncRecord.timestamp,
lastSyncVersion: result.globalVersion,
syncHistory
});
// Update version file for quick access
if (result.globalVersion) {
await fs.writeFile(this.versionFilePath, result.globalVersion);
}
return true;
} catch (error) {
console.error(chalk.red(`❌ Failed to record sync check: ${error.message}`));
return false;
}
}
/**
* Get last known sync version (quick read)
*/
async getLastSyncVersion() {
try {
if (await fs.pathExists(this.versionFilePath)) {
return (await fs.readFile(this.versionFilePath, 'utf8')).trim();
}
return null;
} catch (error) {
return null;
}
}
/**
* Get sync configuration from main config system
*/
async getConfig() {
// Default sync configuration
const defaultConfig = {
syncCheckEnabled: true,
autoSyncOnVersionMismatch: false,
criticalFiles: ['cli/', 'package.json', 'supernal-code-package/'],
ignoredPaths: ['node_modules/', '.git/', 'dist/', 'build/']
};
// TODO: Integrate with main TOML config system when available
// For now, return defaults
return defaultConfig;
}
/**
* Check if .supernalcoding folder should be committed
*/
shouldCommitScFolder() {
// .supernalcoding folder contains only local state and should NEVER be committed
// All configuration is handled by the main TOML config system
return {
commitFolder: false,
commitState: false,
commitVersion: false,
commitLogs: false,
gitignorePattern: '.supernalcoding/',
reason: 'All files contain local state only - configuration is handled by main config system'
};
}
/**
* Check if this repo can do sync checks
*/
async canDoSyncCheck() {
const state = await this.getState();
if (!state) return { canSync: false, reason: 'No .sc state found' };
const config = await this.getConfig();
if (!config.syncCheckEnabled) {
return { canSync: false, reason: 'Sync checking disabled in config' };
}
// Check if we can detect a meaningful local version
const localVersion = await this.detectLocalVersion();
if (!localVersion) {
return { canSync: false, reason: 'Cannot detect local version for comparison' };
}
return { canSync: true, localVersion, repoType: state.repoType };
}
/**
* Detect local version based on repo type
*/
async detectLocalVersion() {
const state = await this.getState();
if (!state) return null;
// First, check if we have a synced version from previous sync operation
if (state.lastSyncVersion) {
return state.lastSyncVersion;
}
switch (state.repoType) {
case 'supernal-source':
// Source repo - use package.json version
try {
const packagePath = path.join(this.projectRoot, 'package.json');
const packageJson = await fs.readJson(packagePath);
return packageJson.version;
} catch (error) {
return null;
}
case 'npm-project':
// Regular npm project - check if sc is installed as dependency
try {
const packagePath = path.join(this.projectRoot, 'package.json');
const packageJson = await fs.readJson(packagePath);
return packageJson.dependencies?.['@supernal/sc'] ||
packageJson.devDependencies?.['@supernal/sc'] ||
packageJson.dependencies?.['supernal-code'] ||
'project-local';
} catch (error) {
return 'project-local';
}
default:
// For other repos, we can still check against global version
return 'repo-local';
}
}
/**
* Show detailed state information
*/
async showStateInfo() {
console.log(chalk.blue.bold('📊 Local Supernal Coding State Information'));
console.log(chalk.blue('=' .repeat(50)));
const state = await this.getState();
if (!state) {
console.log(chalk.red('❌ No .supernalcoding state found. Run: sc sync init'));
return;
}
console.log('');
console.log(chalk.blue('📁 Repository Information:'));
console.log(` Type: ${chalk.cyan(state.repoType)}`);
console.log(` Initialized: ${chalk.cyan(new Date(state.initialized).toLocaleString())}`);
console.log(` Last Updated: ${chalk.cyan(state.lastUpdated ? new Date(state.lastUpdated).toLocaleString() : 'Never')}`);
const localVersion = await this.detectLocalVersion();
console.log(` Local Version: ${chalk.cyan(localVersion || 'Unknown')}`);
console.log('');
console.log(chalk.blue('🔄 Sync History:'));
if (state.syncHistory && state.syncHistory.length > 0) {
state.syncHistory.slice(0, 5).forEach((record, index) => {
const status = record.isInSync ? chalk.green('✅ In Sync') : chalk.red('❌ Out of Sync');
console.log(` ${index + 1}. ${new Date(record.timestamp).toLocaleString()} - ${status}`);
console.log(` Global: ${record.globalVersion || 'Unknown'}, Local: ${record.localVersion || 'Unknown'}`);
if (record.issues && record.issues.length > 0) {
console.log(` Issues: ${record.issues.length}`);
}
});
} else {
console.log(' No sync history recorded yet');
}
const config = await this.getConfig();
console.log('');
console.log(chalk.blue('⚙️ Configuration:'));
console.log(` Sync Enabled: ${config.syncCheckEnabled ? chalk.green('Yes') : chalk.red('No')}`);
console.log(` Auto Sync: ${config.autoSyncOnVersionMismatch ? chalk.green('Yes') : chalk.red('No')}`);
console.log(` Critical Files: ${config.criticalFiles.length} defined`);
console.log(chalk.gray(' (Configuration managed by main config system)'));
const canSync = await this.canDoSyncCheck();
console.log('');
console.log(chalk.blue('🎯 Sync Capability:'));
if (canSync.canSync) {
console.log(` Status: ${chalk.green('✅ Can perform sync checks')}`);
} else {
console.log(` Status: ${chalk.red('❌ Cannot sync')}`);
console.log(` Reason: ${chalk.yellow(canSync.reason)}`);
}
console.log('');
}
}
module.exports = { LocalStateManager };
// CLI usage
if (require.main === module) {
const action = process.argv[2] || 'info';
const verbose = process.argv.includes('--verbose');
const manager = new LocalStateManager({ verbose });
(async () => {
try {
switch (action) {
case 'init':
await manager.initializeScFolder();
break;
case 'info':
await manager.showStateInfo();
break;
case 'config':
const config = await manager.getConfig();
console.log(JSON.stringify(config, null, 2));
break;
default:
console.log('Usage: node local-state-manager.js [init|info|config] [--verbose]');
}
} catch (error) {
console.error(chalk.red('Error:'), error.message);
process.exit(1);
}
})();
}