import { basename, isAbsolute } from 'node:path' import type { IdeSnapshot, OpenFileItem, OpenFileResponse, SnapshotDocument } from './protocol.js' const MAX_TEXT = 32_768 const MAX_DOCUMENTS = 10_000 function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function hasOnlyKeys(value: Record, allowed: readonly string[]): boolean { const keys = new Set(allowed) return Object.keys(value).every((key) => keys.has(key)) } function boundedString(value: unknown, max = MAX_TEXT): value is string { return typeof value === 'string' && value.length > 0 && value.length <= max && !value.includes('\0') } function parseDocument(value: unknown): SnapshotDocument | undefined { if (!isRecord(value) || !hasOnlyKeys(value, ['path', 'label', 'dirty']) || !boundedString(value.path) || !isAbsolute(value.path)) return undefined if (!boundedString(value.label, 1024) || typeof value.dirty !== 'boolean') return undefined return { path: value.path, label: value.label, dirty: value.dirty } } export function parseSnapshot(value: unknown, now = Date.now(), maxAgeMs = 60_000): IdeSnapshot | undefined { if (!isRecord(value) || !hasOnlyKeys(value, ['version', 'instanceId', 'ide', 'processId', 'workspace', 'updatedAt', 'documents']) || value.version !== 1) return undefined if (!boundedString(value.instanceId, 128)) return undefined if (value.ide !== 'vscode' && value.ide !== 'visualstudio') return undefined if (!Number.isSafeInteger(value.processId) || (value.processId as number) <= 0) return undefined if (value.workspace !== undefined && !boundedString(value.workspace)) return undefined if (!boundedString(value.updatedAt, 128) || !Array.isArray(value.documents) || value.documents.length > MAX_DOCUMENTS) return undefined const updated = Date.parse(value.updatedAt) if (!Number.isFinite(updated) || now - updated > maxAgeMs || updated - now > 300_000) return undefined const documents: SnapshotDocument[] = [] const seen = new Set() for (const raw of value.documents) { const document = parseDocument(raw) if (!document) continue const key = document.path.toLocaleLowerCase('en-US') if (seen.has(key)) continue seen.add(key) documents.push(document) } return { version: 1, instanceId: value.instanceId, ide: value.ide, processId: value.processId as number, ...(value.workspace === undefined ? {} : { workspace: value.workspace }), updatedAt: value.updatedAt, documents, } } function score(item: OpenFileItem, query: string): number { if (!query) return 0 const q = query.toLocaleLowerCase('en-US') const label = item.label.toLocaleLowerCase('en-US') const path = item.path.toLocaleLowerCase('en-US') if (label === q) return 0 if (label.startsWith(q)) return 1 if (label.includes(q)) return 2 if (path.includes(q)) return 3 return 10 } export function combineSnapshots(snapshots: readonly IdeSnapshot[], query = '', limit = 100): OpenFileResponse { const normalizedQuery = query.trim().slice(0, 256) const byPath = new Map() for (const snapshot of snapshots) { for (const document of snapshot.documents) { const key = document.path.toLocaleLowerCase('en-US') const current = byPath.get(key) const next: OpenFileItem = { ...document, ide: snapshot.ide, instanceId: snapshot.instanceId, ...(snapshot.workspace === undefined ? {} : { workspace: snapshot.workspace }), instanceOpenCount: snapshot.documents.length, } if (!current || (!current.dirty && next.dirty)) byPath.set(key, next) } } const matching = [...byPath.values()].filter((item) => score(item, normalizedQuery) < 10) matching.sort((a, b) => score(a, normalizedQuery) - score(b, normalizedQuery) || a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }) || a.path.localeCompare(b.path, undefined, { sensitivity: 'base' })) const safeLimit = Math.max(1, Math.min(Number.isFinite(limit) ? Math.floor(limit) : 100, 200)) return { total: matching.length, instances: snapshots.length, items: matching.slice(0, safeLimit) } } export function displayLabel(path: string): string { return basename(path) || path }