diff --git a/web/src/server/pty/pty-manager.ts b/web/src/server/pty/pty-manager.ts index 4fef2bfa..b17d1939 100644 --- a/web/src/server/pty/pty-manager.ts +++ b/web/src/server/pty/pty-manager.ts @@ -32,6 +32,7 @@ import { shouldInjectTitle, } from '../utils/terminal-title.js'; import { WriteQueue } from '../utils/write-queue.js'; +import { VERSION } from '../version.js'; import { AsciinemaWriter } from './asciinema-writer.js'; import { ProcessUtils } from './process-utils.js'; import { SessionManager } from './session-manager.js'; @@ -244,6 +245,7 @@ export class PtyManager extends EventEmitter { startedAt: new Date().toISOString(), initialCols: cols, initialRows: rows, + version: VERSION, }; // Save initial session info diff --git a/web/src/server/pty/session-manager.ts b/web/src/server/pty/session-manager.ts index 71287ea4..43db3e63 100644 --- a/web/src/server/pty/session-manager.ts +++ b/web/src/server/pty/session-manager.ts @@ -12,6 +12,7 @@ import * as os from 'os'; import * as path from 'path'; import type { Session, SessionInfo } from '../../shared/types.js'; import { createLogger } from '../utils/logger.js'; +import { VERSION } from '../version.js'; import { ProcessUtils } from './process-utils.js'; import { PtyError } from './types.js'; @@ -49,6 +50,44 @@ export class SessionManager { } } + /** + * Get the path to the version tracking file + */ + private getVersionFilePath(): string { + return path.join(this.controlPath, '.version'); + } + + /** + * Read the last known version from the version file + */ + private readLastVersion(): string | null { + try { + const versionFile = this.getVersionFilePath(); + if (fs.existsSync(versionFile)) { + const content = fs.readFileSync(versionFile, 'utf8').trim(); + logger.debug(`read last version from file: ${content}`); + return content; + } + return null; + } catch (error) { + logger.warn(`failed to read version file: ${error}`); + return null; + } + } + + /** + * Write the current version to the version file + */ + private writeCurrentVersion(): void { + try { + const versionFile = this.getVersionFilePath(); + fs.writeFileSync(versionFile, VERSION, 'utf8'); + logger.debug(`wrote current version to file: ${VERSION}`); + } catch (error) { + logger.warn(`failed to write version file: ${error}`); + } + } + /** * Create a new session directory structure */ @@ -396,6 +435,76 @@ export class SessionManager { } } + /** + * Cleanup sessions from old VibeTunnel versions + * This is called during server startup to clean sessions when version changes + */ + cleanupOldVersionSessions(): { versionChanged: boolean; cleanedCount: number } { + const lastVersion = this.readLastVersion(); + const currentVersion = VERSION; + + // If no version file exists, this is likely a fresh install or first time with version tracking + if (!lastVersion) { + logger.debug('no previous version found, checking for legacy sessions'); + + // Clean up any sessions without version field + let cleanedCount = 0; + const sessions = this.listSessions(); + for (const session of sessions) { + if (!session.version) { + logger.debug(`cleaning up legacy session ${session.id} (no version field)`); + this.cleanupSession(session.id); + cleanedCount++; + } + } + + this.writeCurrentVersion(); + return { versionChanged: false, cleanedCount }; + } + + // If version hasn't changed, nothing to do + if (lastVersion === currentVersion) { + logger.debug(`version unchanged (${currentVersion}), skipping cleanup`); + return { versionChanged: false, cleanedCount: 0 }; + } + + logger.log(chalk.yellow(`VibeTunnel version changed from ${lastVersion} to ${currentVersion}`)); + logger.log(chalk.yellow('cleaning up old sessions...')); + + let cleanedCount = 0; + try { + const sessions = this.listSessions(); + + for (const session of sessions) { + // Clean all sessions that don't match the current version + // Sessions without version field are considered old + if (!session.version || session.version !== currentVersion) { + logger.debug( + `cleaning up session ${session.id} (version: ${session.version || 'unknown'})` + ); + this.cleanupSession(session.id); + cleanedCount++; + } + } + + // Update the version file to current version + this.writeCurrentVersion(); + + if (cleanedCount > 0) { + logger.log(chalk.green(`cleaned up ${cleanedCount} sessions from previous version`)); + } else { + logger.log(chalk.gray('no old sessions to clean up')); + } + + return { versionChanged: true, cleanedCount }; + } catch (error) { + logger.error(`failed to cleanup old version sessions: ${error}`); + // Still update version file to prevent repeated cleanup attempts + this.writeCurrentVersion(); + return { versionChanged: true, cleanedCount }; + } + } + /** * Get session paths for a given session ID */ diff --git a/web/src/server/server.ts b/web/src/server/server.ts index a9ddbaf5..1fefaf85 100644 --- a/web/src/server/server.ts +++ b/web/src/server/server.ts @@ -396,6 +396,23 @@ export async function createApp(): Promise { const ptyManager = new PtyManager(CONTROL_DIR); logger.debug('Initialized PTY manager'); + // Clean up sessions from old VibeTunnel versions + const sessionManager = ptyManager.getSessionManager(); + const cleanupResult = sessionManager.cleanupOldVersionSessions(); + if (cleanupResult.versionChanged) { + logger.log( + chalk.yellow( + `Version change detected - cleaned up ${cleanupResult.cleanedCount} sessions from previous version` + ) + ); + } else if (cleanupResult.cleanedCount > 0) { + logger.log( + chalk.yellow( + `Cleaned up ${cleanupResult.cleanedCount} legacy sessions without version information` + ) + ); + } + // Initialize Terminal Manager for server-side terminal state const terminalManager = new TerminalManager(CONTROL_DIR); logger.debug('Initialized terminal manager'); diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 78421184..d867eabe 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -22,6 +22,7 @@ export interface SessionInfo { pid?: number; initialCols?: number; initialRows?: number; + version?: string; // VibeTunnel version that created this session } /**