@invisiblecities/sidequest-cqo
Version:
Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection
178 lines • 5.94 kB
JavaScript
/**
* Session Manager for Watch Mode Persistence
* Handles session state, recovery, and resumption capabilities
*/
import { writeFile, readFile, access } from "node:fs/promises";
import path from "node:path";
export class SessionManager {
sessionFile;
currentSession = undefined;
constructor(dataDirectory = "./data") {
this.sessionFile = path.join(dataDirectory, "watch-session.json");
}
/**
* Create a new watch session
*/
async createSession(flags) {
const sessionId = `watch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
this.currentSession = {
id: sessionId,
startTime: Date.now(),
lastUpdate: Date.now(),
checksCount: 0,
baseline: undefined,
current: { total: 0, bySource: {}, byCategory: {} },
viewMode: "dashboard",
errors: [],
metadata: {
cwd: process.cwd(),
nodeVersion: process.version,
platform: process.platform,
flags,
},
};
await this.saveSession();
return this.currentSession;
}
/**
* Load existing session from disk
*/
async loadSession() {
try {
await access(this.sessionFile);
const content = await readFile(this.sessionFile, "utf8");
const session = JSON.parse(content);
// Validate session is recent (within 24 hours)
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
if (Date.now() - session.lastUpdate > maxAge) {
console.log("⏰ Previous session too old, starting fresh...");
return undefined;
}
this.currentSession = session;
return session;
}
catch {
return undefined; // No session file or invalid format
}
}
/**
* Update current session state
*/
async updateSession(updates) {
if (!this.currentSession) {
throw new Error("No active session to update");
}
Object.assign(this.currentSession, updates, {
lastUpdate: Date.now(),
});
await this.saveSession();
}
/**
* Log an error to the current session
*/
async logError(error, checksCount, context) {
if (!this.currentSession) {
return; // No session to log to
}
const sessionError = {
timestamp: Date.now(),
error: error.message,
stack: error.stack,
checksCount,
context,
};
this.currentSession.errors.push(sessionError);
// Keep only last 10 errors to prevent file bloat
if (this.currentSession.errors.length > 10) {
this.currentSession.errors = this.currentSession.errors.slice(-10);
}
await this.saveSession();
}
/**
* Get current session
*/
getCurrentSession() {
return this.currentSession;
}
/**
* Clear current session
*/
async clearSession() {
this.currentSession = undefined;
try {
const { existsSync, unlinkSync } = await import("node:fs");
if (existsSync(this.sessionFile)) {
unlinkSync(this.sessionFile);
}
}
catch {
// Ignore cleanup errors
}
}
/**
* Get session statistics for display
*/
getSessionStats() {
if (!this.currentSession) {
return undefined;
}
const duration = Date.now() - this.currentSession.startTime;
const progressMade = this.currentSession.baseline &&
this.currentSession.current.total < this.currentSession.baseline.total;
return {
duration,
checksCount: this.currentSession.checksCount,
errorCount: this.currentSession.errors.length,
lastError: this.currentSession.errors.at(-1),
progressMade: !!progressMade,
};
}
/**
* Check if session can be resumed safely
*/
canResumeSession(session, currentFlags) {
// Check if critical flags match
const criticalFlags = ["targetPath", "strict", "eslintOnly"];
for (const flag of criticalFlags) {
if (session.metadata.flags[flag] !== currentFlags[flag]) {
return false;
}
}
// Check if working directory matches
if (session.metadata.cwd !== process.cwd()) {
return false;
}
// Check if there are too many recent errors
const recentErrors = session.errors.filter((error) => Date.now() - error.timestamp < 5 * 60 * 1000);
if (recentErrors.length > 3) {
return false; // Too many recent errors, might be unstable
}
return true;
}
/**
* Save session to disk
*/
async saveSession() {
if (!this.currentSession) {
return;
}
try {
const { existsSync, mkdirSync } = await import("node:fs");
// eslint-disable-next-line unicorn/import-style
const pathModule = await import("node:path");
const path = pathModule.default;
// Ensure directory exists
const directory = path.dirname(this.sessionFile);
if (!existsSync(directory)) {
mkdirSync(directory, { recursive: true });
}
const content = JSON.stringify(this.currentSession, undefined, 2);
await writeFile(this.sessionFile, content, "utf8");
}
catch (error) {
const errorObject = error instanceof Error ? error : new Error(String(error));
console.warn("⚠️ Failed to save session state:", errorObject.message);
}
}
}
//# sourceMappingURL=session-manager.js.map