type TObject = Record; const legacyOpenCodePtyIdPattern = /^pty_[0-9a-f]{12}[0-9A-Za-z]{14}$/; const legacyControllerTerminalIdPattern = /^terminal_[A-Za-z0-9_-]{22}$/; export interface IV24LegacySessionGroup { id: string; name: string; sessionIds: unknown[]; } export interface IV24LegacySessionGroupsDocument { id: string; controllerId: string; projectId: string; groups: IV24LegacySessionGroup[]; ungroupedSessionIds?: unknown[]; revision?: number; } export interface IV24LayoutMigrationResult { document: IV24LegacySessionGroupsDocument; migrated: boolean; } const isPlainObject = (valueArg: unknown): valueArg is TObject => { if (typeof valueArg !== 'object' || valueArg === null || Array.isArray(valueArg)) return false; const prototype = Object.getPrototypeOf(valueArg); return prototype === Object.prototype || prototype === null; }; const hasExactKeys = ( valueArg: TObject, requiredArg: readonly string[], optionalArg: readonly string[] = [], ): boolean => { const allowed = new Set([...requiredArg, ...optionalArg]); return requiredArg.every((key) => Object.hasOwn(valueArg, key)) && Object.keys(valueArg).every((key) => allowed.has(key)); }; const isCanonicalControllerTerminalId = (valueArg: string): boolean => { if (!legacyControllerTerminalIdPattern.test(valueArg)) return false; const suffix = valueArg.slice('terminal_'.length); const decoded = Buffer.from(suffix, 'base64url'); return decoded.length === 16 && decoded.toString('base64url') === suffix; }; const isLegacyTerminalNativeId = (valueArg: unknown): valueArg is string => ( typeof valueArg === 'string' && ( legacyOpenCodePtyIdPattern.test(valueArg) || isCanonicalControllerTerminalId(valueArg) ) ); const isLegacyTerminalLayoutReference = (valueArg: unknown): boolean => { if (typeof valueArg === 'string') return isLegacyTerminalNativeId(valueArg); if ( !isPlainObject(valueArg) || Object.keys(valueArg).length !== 2 || !Object.hasOwn(valueArg, 'harnessId') || !Object.hasOwn(valueArg, 'nativeId') || typeof valueArg.nativeId !== 'string' ) return false; if (valueArg.harnessId === 'controller') return true; return valueArg.harnessId === 'opencode' && isLegacyTerminalNativeId(valueArg.nativeId); }; export const migrateV24LegacyTerminalLayoutDocument = ( valueArg: unknown, ): IV24LayoutMigrationResult => { if ( !isPlainObject(valueArg) || !hasExactKeys( valueArg, ['id', 'controllerId', 'projectId', 'groups'], ['ungroupedSessionIds', 'revision'], ) || !Array.isArray(valueArg.groups) ) { throw new Error('Invalid legacy terminal layout migration document.'); } const sourceGroups = valueArg.groups; const groups = sourceGroups.map((groupArg) => { if ( !isPlainObject(groupArg) || !hasExactKeys(groupArg, ['id', 'name', 'sessionIds']) || !Array.isArray(groupArg.sessionIds) ) { throw new Error('Invalid legacy terminal layout group.'); } return { ...groupArg, sessionIds: groupArg.sessionIds.filter((sessionId) => ( !isLegacyTerminalLayoutReference(sessionId) )), } as unknown as IV24LegacySessionGroup; }); const hasUngroupedSessionIds = Object.hasOwn(valueArg, 'ungroupedSessionIds'); if (hasUngroupedSessionIds && !Array.isArray(valueArg.ungroupedSessionIds)) { throw new Error('Invalid legacy terminal ungrouped layout list.'); } const ungroupedSessionIds = hasUngroupedSessionIds ? (valueArg.ungroupedSessionIds as unknown[]).filter((sessionId) => ( !isLegacyTerminalLayoutReference(sessionId) )) : undefined; const migrated = groups.some((group, index) => ( group.sessionIds.length !== (sourceGroups[index] as TObject & { sessionIds: unknown[] }).sessionIds.length )) || ( hasUngroupedSessionIds && ungroupedSessionIds!.length !== (valueArg.ungroupedSessionIds as unknown[]).length ); if (!migrated) { return { document: valueArg as unknown as IV24LegacySessionGroupsDocument, migrated: false, }; } return { document: { ...valueArg, groups, ...(hasUngroupedSessionIds ? { ungroupedSessionIds } : {}), } as unknown as IV24LegacySessionGroupsDocument, migrated: true, }; };