Removed or updated unnecessary eslint-disable comments across several server and service files to improve code clarity. Fixed import paths and added TypeScript ignore comments where needed for compatibility. Minor formatting adjustments were made for readability.
642 lines
21 KiB
TypeScript
642 lines
21 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.js";
|
|
import { AutoSyncService } from "~/server/services/autoSyncService";
|
|
import { repositoryService } from "~/server/services/repositoryService";
|
|
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 localScriptsService.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 {
|
|
console.log('getScriptBySlug called with slug:', input.slug);
|
|
console.log('githubJsonService methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(githubJsonService)));
|
|
console.log('githubJsonService.getScriptBySlug type:', typeof githubJsonService.getScriptBySlug);
|
|
|
|
if (typeof githubJsonService.getScriptBySlug !== 'function') {
|
|
return {
|
|
success: false,
|
|
error: 'getScriptBySlug method is not available on githubJsonService',
|
|
script: null
|
|
};
|
|
}
|
|
|
|
const script = await githubJsonService.getScriptBySlug(input.slug);
|
|
if (!script) {
|
|
return {
|
|
success: false,
|
|
error: 'Script not found',
|
|
script: null
|
|
};
|
|
}
|
|
return { success: true, script };
|
|
} catch (error) {
|
|
console.error('Error in getScriptBySlug:', 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, enabledRepos] = await Promise.all([
|
|
localScriptsService.getScriptCards(),
|
|
localScriptsService.getMetadata(),
|
|
repositoryService.getEnabledRepositories()
|
|
]);
|
|
|
|
// Get all scripts to access their categories
|
|
const scripts = await localScriptsService.getAllScripts();
|
|
|
|
// Create a set of enabled repository URLs for fast lookup
|
|
const enabledRepoUrls = new Set(enabledRepos.map((repo: { url: string }) => repo.url));
|
|
|
|
// 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: ScriptCard) => {
|
|
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;
|
|
// Extract install basenames for robust local matching (e.g., execute.sh -> execute)
|
|
const install_basenames = (script?.install_methods ?? [])
|
|
.map(m => m?.script)
|
|
.filter((p): p is string => typeof p === 'string')
|
|
.map(p => {
|
|
const parts = p.split('/');
|
|
const file = parts[parts.length - 1] ?? '';
|
|
return file.replace(/\.(sh|bash|py|js|ts)$/i, '');
|
|
});
|
|
|
|
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,
|
|
install_basenames,
|
|
// Add repository_url from script
|
|
repository_url: script?.repository_url ?? card.repository_url,
|
|
} as ScriptCard;
|
|
});
|
|
|
|
// Filter cards to only include scripts from enabled repositories
|
|
// For backward compatibility, include scripts without repository_url
|
|
const filteredCards = cardsWithCategories.filter((card: ScriptCard) => {
|
|
const repoUrl = card.repository_url;
|
|
|
|
// If script has no repository_url, include it for backward compatibility
|
|
if (!repoUrl) {
|
|
return true;
|
|
}
|
|
|
|
// Only include scripts from enabled repositories
|
|
return enabledRepoUrls.has(repoUrl);
|
|
});
|
|
|
|
return { success: true, cards: filteredCards, 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: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Load multiple scripts from GitHub
|
|
loadMultipleScripts: publicProcedure
|
|
.input(z.object({ slugs: z.array(z.string()) }))
|
|
.mutation(async ({ input }) => {
|
|
try {
|
|
const successful = [];
|
|
const failed = [];
|
|
|
|
for (const slug of input.slugs) {
|
|
try {
|
|
// Get the script details
|
|
const script = await localScriptsService.getScriptBySlug(slug);
|
|
if (!script) {
|
|
failed.push({ slug, error: 'Script not found' });
|
|
continue;
|
|
}
|
|
|
|
// Load the script files
|
|
const result = await scriptDownloaderService.loadScript(script);
|
|
if (result.success) {
|
|
successful.push({ slug, files: result.files });
|
|
} else {
|
|
const error = 'error' in result ? result.error : 'Failed to load script';
|
|
failed.push({ slug, error });
|
|
}
|
|
} catch (error) {
|
|
failed.push({
|
|
slug,
|
|
error: error instanceof Error ? error.message : 'Failed to load script'
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: `Downloaded ${successful.length} scripts successfully, ${failed.length} failed`,
|
|
successful,
|
|
failed,
|
|
total: input.slugs.length
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in loadMultipleScripts:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to load multiple scripts',
|
|
successful: [],
|
|
failed: [],
|
|
total: 0
|
|
};
|
|
}
|
|
}),
|
|
|
|
// 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: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Delete script files
|
|
deleteScript: 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',
|
|
deletedFiles: []
|
|
};
|
|
}
|
|
|
|
// Delete the script files
|
|
const result = await scriptDownloaderService.deleteScript(script);
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error in deleteScript:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to delete script',
|
|
deletedFiles: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// 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'
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Auto-sync settings and operations
|
|
getAutoSyncSettings: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const autoSyncService = new AutoSyncService();
|
|
const settings = autoSyncService.loadSettings();
|
|
return { success: true, settings };
|
|
} catch (error) {
|
|
console.error('Error getting auto-sync settings:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to get auto-sync settings',
|
|
settings: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
saveAutoSyncSettings: publicProcedure
|
|
.input(z.object({
|
|
autoSyncEnabled: z.boolean(),
|
|
syncIntervalType: z.enum(['predefined', 'custom']),
|
|
syncIntervalPredefined: z.string().optional(),
|
|
syncIntervalCron: z.string().optional(),
|
|
autoDownloadNew: z.boolean(),
|
|
autoUpdateExisting: z.boolean(),
|
|
notificationEnabled: z.boolean(),
|
|
appriseUrls: z.array(z.string()).optional()
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
try {
|
|
// Use the global auto-sync service instance
|
|
const { getAutoSyncService, setAutoSyncService } = await import('~/server/lib/autoSyncInit');
|
|
let autoSyncService = getAutoSyncService();
|
|
|
|
// If no global instance exists, create one
|
|
if (!autoSyncService) {
|
|
const { AutoSyncService } = await import('~/server/services/autoSyncService');
|
|
autoSyncService = new AutoSyncService();
|
|
setAutoSyncService(autoSyncService);
|
|
}
|
|
|
|
// Save settings to both .env file and service instance
|
|
autoSyncService.saveSettings(input);
|
|
|
|
// Reschedule auto-sync if enabled
|
|
if (input.autoSyncEnabled) {
|
|
autoSyncService.scheduleAutoSync();
|
|
console.log('Auto-sync rescheduled with new settings');
|
|
} else {
|
|
autoSyncService.stopAutoSync();
|
|
// Ensure the service is completely stopped and won't restart
|
|
autoSyncService.isRunning = false;
|
|
console.log('Auto-sync stopped');
|
|
}
|
|
|
|
return { success: true, message: 'Auto-sync settings saved successfully' };
|
|
} catch (error) {
|
|
console.error('Error saving auto-sync settings:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to save auto-sync settings'
|
|
};
|
|
}
|
|
}),
|
|
|
|
testNotification: publicProcedure
|
|
.mutation(async () => {
|
|
try {
|
|
const autoSyncService = new AutoSyncService();
|
|
const result = await autoSyncService.testNotification();
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error testing notification:', error);
|
|
return {
|
|
success: false,
|
|
message: error instanceof Error ? error.message : 'Failed to test notification'
|
|
};
|
|
}
|
|
}),
|
|
|
|
triggerManualAutoSync: publicProcedure
|
|
.mutation(async () => {
|
|
try {
|
|
const autoSyncService = new AutoSyncService();
|
|
const result = await autoSyncService.executeAutoSync();
|
|
return {
|
|
success: true,
|
|
message: 'Manual auto-sync completed successfully',
|
|
result
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in manual auto-sync:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to execute manual auto-sync',
|
|
result: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
getAutoSyncStatus: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const autoSyncService = new AutoSyncService();
|
|
const status = autoSyncService.getStatus();
|
|
return { success: true, status };
|
|
} catch (error) {
|
|
console.error('Error getting auto-sync status:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to get auto-sync status',
|
|
status: null
|
|
};
|
|
}
|
|
})
|
|
});
|