vibetunnel
Version:
Terminal sharing server with web interface - supports macOS, Linux, and headless environments
790 lines • 113 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createFilesystemRoutes = createFilesystemRoutes;
const chalk_1 = __importDefault(require("chalk"));
const child_process_1 = require("child_process");
const express_1 = require("express");
const fs_1 = require("fs");
const fs = __importStar(require("fs/promises"));
const mime_types_1 = __importDefault(require("mime-types"));
const path = __importStar(require("path"));
const util_1 = require("util");
const logger_js_1 = require("../utils/logger.js");
const path_utils_js_1 = require("../utils/path-utils.js");
const logger = (0, logger_js_1.createLogger)('filesystem');
const execAsync = (0, util_1.promisify)(child_process_1.exec);
function createFilesystemRoutes() {
const router = (0, express_1.Router)();
// Helper to check if path is safe (no directory traversal) - DISABLED for full filesystem access
function isPathSafe(_requestedPath, _basePath) {
// Security check disabled - allow access to all directories
return true;
}
// Helper to get Git status for a directory
async function getGitStatus(dirPath) {
try {
// Check if directory is a git repository and get repo root
const { stdout: repoRoot } = await execAsync('git rev-parse --show-toplevel', {
cwd: dirPath,
});
const gitRepoRoot = repoRoot.trim();
// Get current branch
const { stdout: branch } = await execAsync('git branch --show-current', { cwd: dirPath });
// Get status relative to repository root
const { stdout: statusOutput } = await execAsync('git status --porcelain', {
cwd: gitRepoRoot,
});
const status = {
isGitRepo: true,
branch: branch.trim(),
modified: [],
added: [],
deleted: [],
untracked: [],
};
// Parse git status output
statusOutput.split('\n').forEach((line) => {
if (!line)
return;
const statusCode = line.substring(0, 2);
const filename = line.substring(3);
if (statusCode === ' M' || statusCode === 'M ' || statusCode === 'MM') {
status.modified.push(filename);
}
else if (statusCode === 'A ' || statusCode === 'AM') {
status.added.push(filename);
}
else if (statusCode === ' D' || statusCode === 'D ') {
status.deleted.push(filename);
}
else if (statusCode === '??') {
status.untracked.push(filename);
}
});
return { status, repoRoot: gitRepoRoot };
}
catch {
return null;
}
}
// Helper to get file Git status
function getFileGitStatus(filePath, gitStatus, gitRepoPath) {
if (!gitStatus)
return undefined;
// Get path relative to git repository root
const relativePath = path.relative(gitRepoPath, filePath);
if (gitStatus.modified.includes(relativePath))
return 'modified';
if (gitStatus.added.includes(relativePath))
return 'added';
if (gitStatus.deleted.includes(relativePath))
return 'deleted';
if (gitStatus.untracked.includes(relativePath))
return 'untracked';
return 'unchanged';
}
// Browse directory endpoint
router.get('/fs/browse', async (req, res) => {
try {
let requestedPath = req.query.path || '.';
const showHidden = req.query.showHidden === 'true';
const gitFilter = req.query.gitFilter; // 'all' | 'changed' | 'none'
// Handle tilde expansion for home directory
requestedPath = (0, path_utils_js_1.expandTildePath)(requestedPath);
logger.debug(`browsing directory: ${requestedPath}, showHidden: ${showHidden}, gitFilter: ${gitFilter}`);
// Security check
if (!isPathSafe(requestedPath, process.cwd())) {
logger.warn(`access denied for path: ${requestedPath}`);
return res.status(403).json({ error: 'Access denied' });
}
const fullPath = path.resolve(requestedPath);
// Check if path exists and is a directory
let stats;
try {
stats = await fs.stat(fullPath);
}
catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
logger.warn(`directory not found: ${requestedPath}`);
return res.status(404).json({ error: 'Directory not found' });
}
// Re-throw other errors to be handled by outer catch
throw error;
}
if (!stats.isDirectory()) {
logger.warn(`path is not a directory: ${requestedPath}`);
return res.status(400).json({ error: 'Path is not a directory' });
}
// Get Git status if requested
const gitStatusStart = Date.now();
const gitInfo = gitFilter !== 'none' ? await getGitStatus(fullPath) : null;
const gitStatus = gitInfo?.status || null;
const gitRepoRoot = gitInfo?.repoRoot || '';
if (gitFilter !== 'none') {
logger.debug(`git status check took ${Date.now() - gitStatusStart}ms for ${requestedPath}`);
}
let files = [];
// If filtering by git changes, show all changed files recursively
if (gitFilter === 'changed' && gitStatus) {
// Get all changed files from git status
const allChangedFiles = [
...gitStatus.modified.map((f) => ({ path: f, status: 'modified' })),
...gitStatus.added.map((f) => ({ path: f, status: 'added' })),
...gitStatus.deleted.map((f) => ({ path: f, status: 'deleted' })),
...gitStatus.untracked.map((f) => ({ path: f, status: 'untracked' })),
];
// Filter to only files under the current directory
const currentDirRelativeToRepo = path.relative(gitRepoRoot, fullPath);
const relevantFiles = allChangedFiles.filter((f) => {
// If we're at repo root, show all files
if (fullPath === gitRepoRoot)
return true;
// Otherwise, only show files under current directory
return f.path.startsWith(`${currentDirRelativeToRepo}/`);
});
// Convert to FileInfo objects
files = await Promise.all(relevantFiles.map(async (changedFile) => {
const absolutePath = path.join(gitRepoRoot, changedFile.path);
// Check if file exists (it might be deleted)
let fileStats = null;
let fileType = 'file';
try {
fileStats = await fs.stat(absolutePath);
fileType = fileStats.isDirectory() ? 'directory' : 'file';
}
catch {
// File might be deleted
fileStats = null;
}
// Get relative display name (relative to current directory)
const relativeToCurrentDir = path.relative(fullPath, absolutePath);
const fileInfo = {
name: relativeToCurrentDir,
path: path.relative(process.cwd(), absolutePath),
type: fileType,
size: fileStats?.size || 0,
modified: fileStats?.mtime.toISOString() || new Date().toISOString(),
permissions: fileStats?.mode?.toString(8).slice(-3) || '000',
isGitTracked: true,
gitStatus: changedFile.status,
};
return fileInfo;
}));
}
else {
// Normal directory listing
const entries = await fs.readdir(fullPath, { withFileTypes: true });
files = await Promise.all(entries
.filter((entry) => showHidden || !entry.name.startsWith('.'))
.map(async (entry) => {
const entryPath = path.join(fullPath, entry.name);
try {
// Use fs.stat() which follows symlinks, instead of entry.isDirectory()
const stats = await fs.stat(entryPath);
const relativePath = path.relative(process.cwd(), entryPath);
// Check if this is a symlink
const isSymlink = entry.isSymbolicLink();
const fileInfo = {
name: entry.name,
path: relativePath,
type: stats.isDirectory() ? 'directory' : 'file',
size: stats.size,
modified: stats.mtime.toISOString(),
permissions: stats.mode.toString(8).slice(-3),
isGitTracked: gitStatus?.isGitRepo || false,
gitStatus: getFileGitStatus(entryPath, gitStatus, gitRepoRoot),
isSymlink,
};
return fileInfo;
}
catch (error) {
// Handle broken symlinks or permission errors
logger.warn(`failed to stat ${entryPath}:`, error);
// For broken symlinks, we'll still show them but as files
const fileInfo = {
name: entry.name,
path: path.relative(process.cwd(), entryPath),
type: 'file',
size: 0,
modified: new Date().toISOString(),
permissions: '000',
isGitTracked: false,
gitStatus: undefined,
};
return fileInfo;
}
}));
}
// No additional filtering needed if we already filtered by git status above
const filteredFiles = files;
// Sort: directories first, then by name
filteredFiles.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'directory' ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
logger.debug(`directory browsed successfully: ${requestedPath} (${filteredFiles.length} items)`);
res.json({
path: requestedPath,
fullPath,
gitStatus,
files: filteredFiles,
});
}
catch (error) {
logger.error(`failed to browse directory ${req.query.path}:`, error);
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
// Get file preview
router.get('/fs/preview', async (req, res) => {
try {
const requestedPath = req.query.path;
if (!requestedPath) {
return res.status(400).json({ error: 'Path is required' });
}
logger.debug(`previewing file: ${requestedPath}`);
// Security check
if (!isPathSafe(requestedPath, process.cwd())) {
logger.warn(`access denied for file preview: ${requestedPath}`);
return res.status(403).json({ error: 'Access denied' });
}
const fullPath = path.resolve(process.cwd(), requestedPath);
const stats = await fs.stat(fullPath);
if (stats.isDirectory()) {
logger.warn(`cannot preview directory: ${requestedPath}`);
return res.status(400).json({ error: 'Cannot preview directories' });
}
// Determine file type
const mimeType = mime_types_1.default.lookup(fullPath) || 'application/octet-stream';
const isText = mimeType.startsWith('text/') ||
mimeType === 'application/json' ||
mimeType === 'application/javascript' ||
mimeType === 'application/typescript' ||
mimeType === 'application/xml';
const isImage = mimeType.startsWith('image/');
if (isImage) {
// For images, return URL to fetch the image
logger.log(chalk_1.default.green(`image preview generated: ${requestedPath} (${formatBytes(stats.size)})`));
res.json({
type: 'image',
mimeType,
url: `/api/fs/raw?path=${encodeURIComponent(requestedPath)}`,
size: stats.size,
});
}
else if (isText || stats.size < 1024 * 1024) {
// Text or small files (< 1MB)
const content = await fs.readFile(fullPath, 'utf-8');
const language = getLanguageFromPath(fullPath);
logger.log(chalk_1.default.green(`text file preview generated: ${requestedPath} (${formatBytes(stats.size)}, ${language})`));
res.json({
type: 'text',
content,
language,
mimeType,
size: stats.size,
});
}
else {
// Binary or large files
logger.log(`binary file preview metadata returned: ${requestedPath} (${formatBytes(stats.size)})`);
res.json({
type: 'binary',
mimeType,
size: stats.size,
humanSize: formatBytes(stats.size),
});
}
}
catch (error) {
logger.error(`failed to preview file ${req.query.path}:`, error);
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
// Serve raw file content
router.get('/fs/raw', (req, res) => {
try {
const requestedPath = req.query.path;
if (!requestedPath) {
return res.status(400).json({ error: 'Path is required' });
}
logger.debug(`serving raw file: ${requestedPath}`);
// Security check
if (!isPathSafe(requestedPath, process.cwd())) {
logger.warn(`access denied for raw file: ${requestedPath}`);
return res.status(403).json({ error: 'Access denied' });
}
const fullPath = path.resolve(process.cwd(), requestedPath);
// Check if file exists
if (!(0, fs_1.statSync)(fullPath).isFile()) {
logger.warn(`file not found for raw access: ${requestedPath}`);
return res.status(404).json({ error: 'File not found' });
}
// Set appropriate content type
const mimeType = mime_types_1.default.lookup(fullPath) || 'application/octet-stream';
res.setHeader('Content-Type', mimeType);
// Stream the file
const stream = (0, fs_1.createReadStream)(fullPath);
stream.pipe(res);
stream.on('end', () => {
logger.log(chalk_1.default.green(`raw file served: ${requestedPath}`));
});
}
catch (error) {
logger.error(`failed to serve raw file ${req.query.path}:`, error);
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
// Get file content (text files only)
router.get('/fs/content', async (req, res) => {
try {
const requestedPath = req.query.path;
if (!requestedPath) {
return res.status(400).json({ error: 'Path is required' });
}
logger.debug(`getting file content: ${requestedPath}`);
// Security check
if (!isPathSafe(requestedPath, process.cwd())) {
logger.warn(`access denied for file content: ${requestedPath}`);
return res.status(403).json({ error: 'Access denied' });
}
const fullPath = path.resolve(process.cwd(), requestedPath);
const content = await fs.readFile(fullPath, 'utf-8');
logger.log(chalk_1.default.green(`file content retrieved: ${requestedPath}`));
res.json({
path: requestedPath,
content,
language: getLanguageFromPath(fullPath),
});
}
catch (error) {
logger.error(`failed to get file content ${req.query.path}:`, error);
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
// Get Git diff for a file
router.get('/fs/diff', async (req, res) => {
try {
const requestedPath = req.query.path;
if (!requestedPath) {
return res.status(400).json({ error: 'Path is required' });
}
logger.debug(`getting git diff: ${requestedPath}`);
// Security check
if (!isPathSafe(requestedPath, process.cwd())) {
logger.warn(`access denied for git diff: ${requestedPath}`);
return res.status(403).json({ error: 'Access denied' });
}
const fullPath = path.resolve(process.cwd(), requestedPath);
const relativePath = path.relative(process.cwd(), fullPath);
// Get git diff
const diffStart = Date.now();
const { stdout: diff } = await execAsync(`git diff HEAD -- "${relativePath}"`, {
cwd: process.cwd(),
});
const diffTime = Date.now() - diffStart;
if (diffTime > 1000) {
logger.warn(`slow git diff operation: ${requestedPath} took ${diffTime}ms`);
}
logger.log(chalk_1.default.green(`git diff retrieved: ${requestedPath} (${diff.length > 0 ? 'has changes' : 'no changes'})`));
res.json({
path: requestedPath,
diff,
hasDiff: diff.length > 0,
});
}
catch (error) {
logger.error(`failed to get git diff for ${req.query.path}:`, error);
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
// Get file content for diff view (current and HEAD versions)
router.get('/fs/diff-content', async (req, res) => {
try {
const requestedPath = req.query.path;
if (!requestedPath) {
return res.status(400).json({ error: 'Path is required' });
}
logger.debug(`getting diff content: ${requestedPath}`);
// Security check
if (!isPathSafe(requestedPath, process.cwd())) {
logger.warn(`access denied for diff content: ${requestedPath}`);
return res.status(403).json({ error: 'Access denied' });
}
const fullPath = path.resolve(process.cwd(), requestedPath);
const relativePath = path.relative(process.cwd(), fullPath);
logger.debug(`Getting diff content for: ${requestedPath}`);
logger.debug(`Full path: ${fullPath}`);
logger.debug(`CWD: ${process.cwd()}`);
// Get current file content
const currentContent = await fs.readFile(fullPath, 'utf-8');
logger.debug(`Current content length: ${currentContent.length}`);
// Get HEAD version content
let originalContent = ''; // Default to empty string for new files
try {
// Use ./ prefix as git suggests for paths relative to current directory
const gitPath = `./${relativePath}`;
logger.debug(`Getting HEAD version: git show HEAD:"${gitPath}"`);
const { stdout } = await execAsync(`git show HEAD:"${gitPath}"`, {
cwd: process.cwd(),
});
originalContent = stdout;
logger.debug(`Got HEAD version for ${gitPath}, length: ${originalContent.length}`);
}
catch (error) {
// File might be new (not in HEAD), use empty string
if (error instanceof Error && error.message.includes('does not exist')) {
originalContent = '';
logger.debug(`File ${requestedPath} does not exist in HEAD (new file)`);
}
else {
// For other errors, log the full error
logger.error(`Failed to get HEAD version of ./${relativePath}:`, error);
// Check if it's a stderr message
if (error instanceof Error && 'stderr' in error) {
const execError = error;
if (execError.stderr) {
logger.error(`Git stderr: ${execError.stderr}`);
}
}
// For non-git repos, show no diff
originalContent = currentContent;
}
}
logger.log(chalk_1.default.green(`diff content retrieved: ${requestedPath}`));
res.json({
path: requestedPath,
originalContent,
modifiedContent: currentContent,
language: getLanguageFromPath(fullPath),
});
}
catch (error) {
logger.error(`failed to get diff content for ${req.query.path}:`, error);
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
// Create directory
router.post('/fs/mkdir', async (req, res) => {
try {
const { path: dirPath, name } = req.body;
if (!dirPath || !name) {
return res.status(400).json({ error: 'Path and name are required' });
}
logger.log(`creating directory: ${name} in ${dirPath}`);
// Validate name (no slashes, no dots at start)
if (name.includes('/') || name.includes('\\') || name.startsWith('.')) {
logger.warn(`invalid directory name attempted: ${name}`);
return res.status(400).json({ error: 'Invalid directory name' });
}
// Security check
if (!isPathSafe(dirPath, process.cwd())) {
logger.warn(`access denied for mkdir: ${dirPath}/${name}`);
return res.status(403).json({ error: 'Access denied' });
}
const fullPath = path.resolve(process.cwd(), dirPath, name);
// Create directory
await fs.mkdir(fullPath, { recursive: true });
logger.log(chalk_1.default.green(`directory created: ${path.relative(process.cwd(), fullPath)}`));
res.json({
success: true,
path: path.relative(process.cwd(), fullPath),
});
}
catch (error) {
logger.error(`failed to create directory ${req.body.path}/${req.body.name}:`, error);
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
// Path completions endpoint for autocomplete
router.get('/fs/completions', async (req, res) => {
try {
const originalPath = req.query.path || '';
let partialPath = originalPath;
// Handle tilde expansion for home directory
partialPath = (0, path_utils_js_1.expandTildePath)(partialPath);
// Separate directory and partial name
let dirPath;
let partialName;
if (partialPath.endsWith('/')) {
// If path ends with slash, list contents of that directory
dirPath = partialPath;
partialName = '';
}
else {
// Otherwise, get the directory and partial filename
dirPath = path.dirname(partialPath);
partialName = path.basename(partialPath);
}
// Resolve the directory path
const fullDirPath = path.resolve(dirPath);
// Security check
if (!isPathSafe(fullDirPath, '/')) {
logger.warn(`access denied for path completions: ${fullDirPath}`);
return res.status(403).json({ error: 'Access denied' });
}
// Check if directory exists
let dirStats;
try {
dirStats = await fs.stat(fullDirPath);
if (!dirStats.isDirectory()) {
return res.json({ completions: [] });
}
}
catch {
// Directory doesn't exist, return empty completions
return res.json({ completions: [] });
}
// Read directory contents
const entries = await fs.readdir(fullDirPath, { withFileTypes: true });
// Filter and map entries
const mappedEntries = await Promise.all(entries
.filter((entry) => {
// Filter by partial name (case-insensitive)
if (partialName && !entry.name.toLowerCase().startsWith(partialName.toLowerCase())) {
return false;
}
// Optionally hide hidden files unless the partial name starts with '.'
if (!partialName.startsWith('.') && entry.name.startsWith('.')) {
return false;
}
return true;
})
.map(async (entry) => {
const isDirectory = entry.isDirectory();
const entryPath = path.join(fullDirPath, entry.name);
// Build the suggestion path based on the original input
let displayPath;
if (originalPath.endsWith('/')) {
displayPath = originalPath + entry.name;
}
else {
const lastSlash = originalPath.lastIndexOf('/');
if (lastSlash >= 0) {
displayPath = originalPath.substring(0, lastSlash + 1) + entry.name;
}
else {
displayPath = entry.name;
}
}
// Check if this directory is a git repository and get branch + status
let isGitRepo = false;
let gitBranch;
let gitStatusCount = 0;
let gitAddedCount = 0;
let gitModifiedCount = 0;
let gitDeletedCount = 0;
let isWorktree = false;
if (isDirectory) {
try {
const gitPath = path.join(entryPath, '.git');
const gitStat = await fs.stat(gitPath);
isGitRepo = true;
// Check if it's a worktree (has a .git file instead of directory)
if (gitStat.isFile()) {
isWorktree = true;
}
// Get the current git branch
try {
const { stdout: branch } = await execAsync('git branch --show-current', {
cwd: entryPath,
});
gitBranch = branch.trim();
}
catch {
// Failed to get branch
}
// Get the number of changed files by type
try {
const { stdout: statusOutput } = await execAsync('git status --porcelain', {
cwd: entryPath,
});
const lines = statusOutput.split('\n').filter((line) => line.trim() !== '');
for (const line of lines) {
const statusCode = line.substring(0, 2);
if (statusCode === '??' || statusCode === 'A ' || statusCode === 'AM') {
gitAddedCount++;
}
else if (statusCode === ' D' || statusCode === 'D ') {
gitDeletedCount++;
}
else if (statusCode === ' M' || statusCode === 'M ' || statusCode === 'MM') {
gitModifiedCount++;
}
}
gitStatusCount = gitAddedCount + gitModifiedCount + gitDeletedCount;
}
catch {
// Failed to get status
}
}
catch {
// Not a git repository
}
}
return {
name: entry.name,
path: displayPath,
type: isDirectory ? 'directory' : 'file',
// Add trailing slash for directories
suggestion: isDirectory ? `${displayPath}/` : displayPath,
isRepository: isGitRepo,
gitBranch,
gitStatusCount,
gitAddedCount,
gitModifiedCount,
gitDeletedCount,
isWorktree,
};
}));
const completions = mappedEntries
.sort((a, b) => {
// Sort directories first, then by name
if (a.type !== b.type) {
return a.type === 'directory' ? -1 : 1;
}
return a.name.localeCompare(b.name);
})
.slice(0, 20); // Limit to 20 suggestions
logger.debug(`path completions for "${originalPath}": ${completions.length} results`);
res.json({
completions,
partialPath: originalPath,
});
}
catch (error) {
logger.error(`failed to get path completions for ${req.query.path}:`, error);
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
return router;
}
// Helper function to determine language from file path
function getLanguageFromPath(filePath) {
const ext = path.extname(filePath).toLowerCase();
const languageMap = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.py': 'python',
'.java': 'java',
'.c': 'c',
'.cpp': 'cpp',
'.cs': 'csharp',
'.php': 'php',
'.rb': 'ruby',
'.go': 'go',
'.rs': 'rust',
'.swift': 'swift',
'.kt': 'kotlin',
'.scala': 'scala',
'.r': 'r',
'.m': 'objective-c',
'.mm': 'objective-c',
'.h': 'c',
'.hpp': 'cpp',
'.sh': 'shell',
'.bash': 'shell',
'.zsh': 'shell',
'.fish': 'shell',
'.ps1': 'powershell',
'.html': 'html',
'.htm': 'html',
'.xml': 'xml',
'.css': 'css',
'.scss': 'scss',
'.sass': 'sass',
'.less': 'less',
'.json': 'json',
'.yaml': 'yaml',
'.yml': 'yaml',
'.toml': 'toml',
'.ini': 'ini',
'.cfg': 'ini',
'.conf': 'ini',
'.sql': 'sql',
'.md': 'markdown',
'.markdown': 'markdown',
'.tex': 'latex',
'.dockerfile': 'dockerfile',
'.makefile': 'makefile',
'.cmake': 'cmake',
'.gradle': 'gradle',
'.vue': 'vue',
'.svelte': 'svelte',
'.elm': 'elm',
'.clj': 'clojure',
'.cljs': 'clojure',
'.ex': 'elixir',
'.exs': 'elixir',
'.erl': 'erlang',
'.hrl': 'erlang',
'.fs': 'fsharp',
'.fsx': 'fsharp',
'.fsi': 'fsharp',
'.ml': 'ocaml',
'.mli': 'ocaml',
'.pas': 'pascal',
'.pp': 'pascal',
'.pl': 'perl',
'.pm': 'perl',
'.t': 'perl',
'.lua': 'lua',
'.dart': 'dart',
'.nim': 'nim',
'.nims': 'nim',
'.zig': 'zig',
'.jl': 'julia',
};
return languageMap[ext] || 'plaintext';
}
// Helper function to format bytes
function formatBytes(bytes, decimals = 2) {
if (bytes === 0)
return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZXN5c3RlbS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9zZXJ2ZXIvcm91dGVzL2ZpbGVzeXN0ZW0udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFvQ0Esd0RBK3VCQztBQW54QkQsa0RBQTBCO0FBQzFCLGlEQUFxQztBQUNyQyxxQ0FBOEQ7QUFDOUQsMkJBQWdEO0FBQ2hELGdEQUFrQztBQUNsQyw0REFBOEI7QUFDOUIsMkNBQTZCO0FBQzdCLCtCQUFpQztBQUNqQyxrREFBa0Q7QUFDbEQsMERBQXlEO0FBRXpELE1BQU0sTUFBTSxHQUFHLElBQUEsd0JBQVksRUFBQyxZQUFZLENBQUMsQ0FBQztBQUUxQyxNQUFNLFNBQVMsR0FBRyxJQUFBLGdCQUFTLEVBQUMsb0JBQUksQ0FBQyxDQUFDO0FBdUJsQyxTQUFnQixzQkFBc0I7SUFDcEMsTUFBTSxNQUFNLEdBQUcsSUFBQSxnQkFBTSxHQUFFLENBQUM7SUFFeEIsaUdBQWlHO0lBQ2pHLFNBQVMsVUFBVSxDQUFDLGNBQXNCLEVBQUUsU0FBaUI7UUFDM0QsNERBQTREO1FBQzVELE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUVELDJDQUEyQztJQUMzQyxLQUFLLFVBQVUsWUFBWSxDQUN6QixPQUFlO1FBRWYsSUFBSSxDQUFDO1lBQ0gsMkRBQTJEO1lBQzNELE1BQU0sRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLEdBQUcsTUFBTSxTQUFTLENBQUMsK0JBQStCLEVBQUU7Z0JBQzVFLEdBQUcsRUFBRSxPQUFPO2FBQ2IsQ0FBQyxDQUFDO1lBQ0gsTUFBTSxXQUFXLEdBQUcsUUFBUSxDQUFDLElBQUksRUFBRSxDQUFDO1lBRXBDLHFCQUFxQjtZQUNyQixNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLE1BQU0sU0FBUyxDQUFDLDJCQUEyQixFQUFFLEVBQUUsR0FBRyxFQUFFLE9BQU8sRUFB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