@kaifronsdal/transcript-viewer
Version:
A web-based viewer for AI conversation transcripts with rollback support
126 lines (110 loc) • 3.74 kB
text/typescript
import { promises as fs } from 'fs';
import path from 'path';
import type { DirectoryInfo, LoadingError } from './types';
// Server-side check
if (typeof window !== 'undefined') {
throw new Error('file-scanner can only be used on the server side');
}
/**
* Scan a directory for transcript files (.json files)
*/
export async function scanDirectoryForTranscripts(
dirPath: string,
basePath: string = dirPath
): Promise<{ files: string[]; directories: DirectoryInfo[]; errors: LoadingError[] }> {
const files: string[] = [];
const directories: DirectoryInfo[] = [];
const errors: LoadingError[] = [];
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
// Separate files and directories
const transcriptFiles = entries
.filter(entry => entry.isFile() && entry.name.endsWith('.json'))
.map(entry => {
const fullPath = path.join(dirPath, entry.name);
// Return path relative to basePath
return path.relative(basePath, fullPath);
});
const subdirs = entries.filter(entry => entry.isDirectory());
files.push(...transcriptFiles);
// Process subdirectories
for (const subdir of subdirs) {
const subdirPath = path.join(dirPath, subdir.name);
const relativePath = path.relative(basePath, subdirPath);
try {
const subdirResult = await scanDirectoryForTranscripts(subdirPath, basePath);
files.push(...subdirResult.files);
directories.push(...subdirResult.directories);
errors.push(...subdirResult.errors);
// Create directory info
const directoryInfo: DirectoryInfo = {
path: subdirPath,
relativePath,
isEmpty: subdirResult.files.length === 0 && subdirResult.directories.length === 0,
hasTranscripts: subdirResult.files.length > 0,
transcriptCount: subdirResult.files.length,
subdirectoryCount: subdirResult.directories.length
};
directories.push(directoryInfo);
} catch (error) {
const loadingError: LoadingError = {
type: 'permission_denied',
message: `Failed to scan directory: ${subdirPath}`,
file: subdirPath,
details: error instanceof Error ? error.message : String(error)
};
errors.push(loadingError);
}
}
} catch (error) {
const loadingError: LoadingError = {
type: 'permission_denied',
message: `Failed to scan directory: ${dirPath}`,
file: dirPath,
details: error instanceof Error ? error.message : String(error)
};
errors.push(loadingError);
}
return { files, directories, errors };
}
/**
* Check if a path exists and is accessible
*/
export async function checkPathAccess(dirPath: string): Promise<boolean> {
try {
await fs.access(dirPath);
return true;
} catch {
return false;
}
}
/**
* Get directory stats without recursing into subdirectories
*/
export async function getDirectoryStats(dirPath: string): Promise<{
exists: boolean;
fileCount: number;
subdirCount: number;
transcriptCount: number;
}> {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const fileCount = entries.filter(entry => entry.isFile()).length;
const subdirCount = entries.filter(entry => entry.isDirectory()).length;
const transcriptCount = entries
.filter(entry => entry.isFile() && entry.name.endsWith('.json')).length;
return {
exists: true,
fileCount,
subdirCount,
transcriptCount
};
} catch {
return {
exists: false,
fileCount: 0,
subdirCount: 0,
transcriptCount: 0
};
}
}