* Fix: Detect downloaded scripts from all directories (ct, tools, vm, vw) - Add getAllDownloadedScripts() method to scan all script directories - Update DownloadedScriptsTab to use new API endpoint - Update ScriptsGrid to use new API endpoint for download status detection - Update main page script counts to use new API endpoint - Add recursive directory scanning to handle subdirectories - Maintain backward compatibility with existing getCtScripts endpoint Fixes issue where scripts downloaded to tools/, vm/, or vw/ directories were not showing in Downloaded Scripts tab or showing correct download status in Available Scripts tab. * Fix: Remove redundant type annotation and method call arguments - Remove redundant string type annotation from relativePath parameter - Fix getScriptsFromDirectory method call to match updated signature
400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
import { z } from "zod";
|
|
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
|
import { scriptManager } from "~/server/lib/scripts";
|
|
import { githubJsonService } from "~/server/services/githubJsonService";
|
|
import { localScriptsService } from "~/server/services/localScripts";
|
|
import { scriptDownloaderService } from "~/server/services/scriptDownloader";
|
|
import type { ScriptCard } from "~/types/script";
|
|
|
|
export const scriptsRouter = createTRPCRouter({
|
|
// Get all available scripts
|
|
getScripts: publicProcedure
|
|
.query(async () => {
|
|
const scripts = await scriptManager.getScripts();
|
|
return {
|
|
scripts,
|
|
directoryInfo: scriptManager.getScriptsDirectoryInfo()
|
|
};
|
|
}),
|
|
|
|
// Get CT scripts (for local scripts tab)
|
|
getCtScripts: publicProcedure
|
|
.query(async () => {
|
|
const scripts = await scriptManager.getCtScripts();
|
|
return {
|
|
scripts,
|
|
directoryInfo: scriptManager.getScriptsDirectoryInfo()
|
|
};
|
|
}),
|
|
|
|
// Get all downloaded scripts from all directories
|
|
getAllDownloadedScripts: publicProcedure
|
|
.query(async () => {
|
|
const scripts = await scriptManager.getAllDownloadedScripts();
|
|
return {
|
|
scripts,
|
|
directoryInfo: scriptManager.getScriptsDirectoryInfo()
|
|
};
|
|
}),
|
|
|
|
|
|
// Get script content for viewing
|
|
getScriptContent: publicProcedure
|
|
.input(z.object({ path: z.string() }))
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const { readFile } = await import('fs/promises');
|
|
const { join } = await import('path');
|
|
const { env } = await import('~/env');
|
|
|
|
const scriptsDir = join(process.cwd(), env.SCRIPTS_DIRECTORY);
|
|
const fullPath = join(scriptsDir, input.path);
|
|
|
|
// Security check: ensure the path is within the scripts directory
|
|
if (!fullPath.startsWith(scriptsDir)) {
|
|
throw new Error('Invalid script path');
|
|
}
|
|
|
|
const content = await readFile(fullPath, 'utf-8');
|
|
return { success: true, content };
|
|
} catch (error) {
|
|
console.error('Error reading script content:', error);
|
|
return { success: false, error: 'Failed to read script content' };
|
|
}
|
|
}),
|
|
|
|
// Validate script path
|
|
validateScript: publicProcedure
|
|
.input(z.object({ scriptPath: z.string() }))
|
|
.query(async ({ input }) => {
|
|
const validation = scriptManager.validateScriptPath(input.scriptPath);
|
|
return validation;
|
|
}),
|
|
|
|
// Get directory information
|
|
getDirectoryInfo: publicProcedure
|
|
.query(async () => {
|
|
return scriptManager.getScriptsDirectoryInfo();
|
|
}),
|
|
|
|
// Local script routes (using scripts/json directory)
|
|
// Get all script cards from local directory
|
|
getScriptCards: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const cards = await localScriptsService.getScriptCards();
|
|
return { success: true, cards };
|
|
} catch (error) {
|
|
console.error('Error in getScriptCards:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch script cards',
|
|
cards: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get all scripts from GitHub (1 API call + raw downloads)
|
|
getAllScripts: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const scripts = await githubJsonService.getAllScripts();
|
|
return { success: true, scripts };
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch scripts',
|
|
scripts: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get script by slug from GitHub (1 API call + raw downloads)
|
|
getScriptBySlug: publicProcedure
|
|
.input(z.object({ slug: z.string() }))
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const script = await githubJsonService.getScriptBySlug(input.slug);
|
|
if (!script) {
|
|
return {
|
|
success: false,
|
|
error: 'Script not found',
|
|
script: null
|
|
};
|
|
}
|
|
return { success: true, script };
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch script',
|
|
script: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get metadata (categories and other metadata)
|
|
getMetadata: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const metadata = await localScriptsService.getMetadata();
|
|
return { success: true, metadata };
|
|
} catch (error) {
|
|
console.error('Error in getMetadata:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch metadata',
|
|
metadata: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get script cards with category information
|
|
getScriptCardsWithCategories: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const [cards, metadata] = await Promise.all([
|
|
localScriptsService.getScriptCards(),
|
|
localScriptsService.getMetadata()
|
|
]);
|
|
|
|
// Get all scripts to access their categories
|
|
const scripts = await localScriptsService.getAllScripts();
|
|
|
|
// Create category ID to name mapping
|
|
const categoryMap: Record<number, string> = {};
|
|
if (metadata?.categories) {
|
|
metadata.categories.forEach((cat: any) => {
|
|
categoryMap[cat.id] = cat.name;
|
|
});
|
|
}
|
|
|
|
// Enhance cards with category information and additional script data
|
|
const cardsWithCategories = cards.map(card => {
|
|
const script = scripts.find(s => s.slug === card.slug);
|
|
const categoryNames: string[] = script?.categories?.map(id => categoryMap[id]).filter((name): name is string => typeof name === 'string') ?? [];
|
|
|
|
// Extract OS and version from first install method
|
|
const firstInstallMethod = script?.install_methods?.[0];
|
|
const os = firstInstallMethod?.resources?.os;
|
|
const version = firstInstallMethod?.resources?.version;
|
|
|
|
return {
|
|
...card,
|
|
categories: script?.categories ?? [],
|
|
categoryNames: categoryNames,
|
|
// Add date_created from script
|
|
date_created: script?.date_created,
|
|
// Add OS and version from install methods
|
|
os: os,
|
|
version: version,
|
|
// Add interface port
|
|
interface_port: script?.interface_port,
|
|
} as ScriptCard;
|
|
});
|
|
|
|
return { success: true, cards: cardsWithCategories, metadata };
|
|
} catch (error) {
|
|
console.error('Error in getScriptCardsWithCategories:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch script cards with categories',
|
|
cards: [],
|
|
metadata: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Resync scripts from GitHub (1 API call + raw downloads)
|
|
resyncScripts: publicProcedure
|
|
.mutation(async () => {
|
|
try {
|
|
// Sync JSON files using 1 API call + raw downloads
|
|
const result = await githubJsonService.syncJsonFiles();
|
|
|
|
return {
|
|
success: result.success,
|
|
message: result.message,
|
|
count: result.count
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in resyncScripts:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to resync scripts. Make sure REPO_URL is set.',
|
|
count: 0
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Load script files from GitHub
|
|
loadScript: publicProcedure
|
|
.input(z.object({ slug: z.string() }))
|
|
.mutation(async ({ input }) => {
|
|
try {
|
|
// Get the script details
|
|
const script = await localScriptsService.getScriptBySlug(input.slug);
|
|
if (!script) {
|
|
return {
|
|
success: false,
|
|
error: 'Script not found',
|
|
files: []
|
|
};
|
|
}
|
|
|
|
// Load the script files
|
|
const result = await scriptDownloaderService.loadScript(script);
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error in loadScript:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to load script',
|
|
files: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Check if script files exist locally
|
|
checkScriptFiles: publicProcedure
|
|
.input(z.object({ slug: z.string() }))
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const script = await localScriptsService.getScriptBySlug(input.slug);
|
|
if (!script) {
|
|
return {
|
|
success: false,
|
|
error: 'Script not found',
|
|
ctExists: false,
|
|
installExists: false,
|
|
files: []
|
|
};
|
|
}
|
|
|
|
const result = await scriptDownloaderService.checkScriptExists(script);
|
|
return {
|
|
success: true,
|
|
...result
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in checkScriptFiles:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to check script files',
|
|
ctExists: false,
|
|
installExists: false,
|
|
files: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Compare local and remote script content
|
|
compareScriptContent: publicProcedure
|
|
.input(z.object({ slug: z.string() }))
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const script = await localScriptsService.getScriptBySlug(input.slug);
|
|
if (!script) {
|
|
return {
|
|
success: false,
|
|
error: 'Script not found',
|
|
hasDifferences: false,
|
|
differences: []
|
|
};
|
|
}
|
|
|
|
const result = await scriptDownloaderService.compareScriptContent(script);
|
|
return {
|
|
success: true,
|
|
...result
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in compareScriptContent:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to compare script content',
|
|
hasDifferences: false,
|
|
differences: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get diff content for a specific script file
|
|
getScriptDiff: publicProcedure
|
|
.input(z.object({ slug: z.string(), filePath: z.string() }))
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const script = await localScriptsService.getScriptBySlug(input.slug);
|
|
if (!script) {
|
|
return {
|
|
success: false,
|
|
error: 'Script not found',
|
|
diff: null
|
|
};
|
|
}
|
|
|
|
const result = await scriptDownloaderService.getScriptDiff(script, input.filePath);
|
|
return {
|
|
success: true,
|
|
...result
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in getScriptDiff:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to get script diff',
|
|
diff: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Check if running on Proxmox VE host
|
|
checkProxmoxVE: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const { spawn } = await import('child_process');
|
|
|
|
return new Promise((resolve) => {
|
|
const child = spawn('command', ['-v', 'pveversion'], {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
shell: true
|
|
});
|
|
|
|
|
|
child.on('close', (code) => {
|
|
// If command exits with code 0, pveversion command exists
|
|
if (code === 0) {
|
|
resolve({
|
|
success: true,
|
|
isProxmoxVE: true,
|
|
message: 'Running on Proxmox VE host'
|
|
});
|
|
} else {
|
|
resolve({
|
|
success: true,
|
|
isProxmoxVE: false,
|
|
message: 'Not running on Proxmox VE host'
|
|
});
|
|
}
|
|
});
|
|
|
|
child.on('error', (error) => {
|
|
resolve({
|
|
success: false,
|
|
isProxmoxVE: false,
|
|
error: error.message,
|
|
message: 'Failed to check Proxmox VE status'
|
|
});
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error('Error in checkProxmoxVE:', error);
|
|
return {
|
|
success: false,
|
|
isProxmoxVE: false,
|
|
error: error instanceof Error ? error.message : 'Failed to check Proxmox VE status',
|
|
message: 'Failed to check Proxmox VE status'
|
|
};
|
|
}
|
|
})
|
|
});
|