feat: Switch to local JSON files as primary source with GitHub resync

- Create LocalScriptsService to read from scripts/json directory
- Update tRPC routes to use local files by default
- Implement resync functionality to download from GitHub and save locally
- Make GitHub optional - only needed for resyncing
- Update UI to reflect local-first approach
- Add proper error handling for missing local files
This commit is contained in:
Michel Roegl-Brunner
2025-09-10 14:44:02 +02:00
parent 4059b5a12f
commit 18f6ebae18
4 changed files with 128 additions and 15 deletions

View File

@@ -48,8 +48,8 @@ export function ScriptsGrid() {
{scriptCardsData?.error || 'Unknown error occurred'}
</p>
<div className="mt-4 text-xs text-gray-400">
<p>Make sure to set the REPO_URL environment variable.</p>
<p>Example: REPO_URL="https://github.com/username/repo"</p>
<p>No JSON files found in scripts/json directory.</p>
<p>Use the "Resync Scripts" button to download from GitHub.</p>
</div>
</div>
<button

View File

@@ -50,7 +50,7 @@ export default function Home() {
: 'text-gray-500 hover:text-gray-700'
}`}
>
GitHub Scripts
Script Library
</button>
<button
onClick={() => setActiveTab('local')}

View File

@@ -3,6 +3,7 @@ import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { scriptManager } from "~/server/lib/scripts";
import { gitManager } from "~/server/lib/git";
import { githubService } from "~/server/services/github";
import { localScriptsService } from "~/server/services/localScripts";
export const scriptsRouter = createTRPCRouter({
// Get all available scripts
@@ -58,12 +59,12 @@ export const scriptsRouter = createTRPCRouter({
return scriptManager.getScriptsDirectoryInfo();
}),
// GitHub-based script routes
// Get all script cards from GitHub repo
// Local script routes (using scripts/json directory)
// Get all script cards from local directory
getScriptCards: publicProcedure
.query(async () => {
try {
const cards = await githubService.getScriptCards();
const cards = await localScriptsService.getScriptCards();
return { success: true, cards };
} catch (error) {
console.error('Error in getScriptCards:', error);
@@ -75,11 +76,11 @@ export const scriptsRouter = createTRPCRouter({
}
}),
// Get all scripts from GitHub repo
// Get all scripts from local directory
getAllScripts: publicProcedure
.query(async () => {
try {
const scripts = await githubService.getAllScripts();
const scripts = await localScriptsService.getAllScripts();
return { success: true, scripts };
} catch (error) {
return {
@@ -90,12 +91,12 @@ export const scriptsRouter = createTRPCRouter({
}
}),
// Get script by slug from GitHub repo
// Get script by slug from local directory
getScriptBySlug: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ input }) => {
try {
const script = await githubService.getScriptBySlug(input.slug);
const script = await localScriptsService.getScriptBySlug(input.slug);
if (!script) {
return {
success: false,
@@ -113,20 +114,26 @@ export const scriptsRouter = createTRPCRouter({
}
}),
// Resync scripts from GitHub repo
// Resync scripts from GitHub repo to local directory
resyncScripts: publicProcedure
.mutation(async () => {
try {
const scripts = await githubService.getAllScripts();
// First, try to get scripts from GitHub
const githubScripts = await githubService.getAllScripts();
// Save scripts to local directory
await localScriptsService.saveScriptsFromGitHub(githubScripts);
return {
success: true,
message: `Successfully synced ${scripts.length} scripts`,
count: scripts.length
message: `Successfully synced ${githubScripts.length} scripts from GitHub to local directory`,
count: githubScripts.length
};
} catch (error) {
console.error('Error in resyncScripts:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to resync scripts',
error: error instanceof Error ? error.message : 'Failed to resync scripts. Make sure REPO_URL is set.',
count: 0
};
}

View File

@@ -0,0 +1,106 @@
import { readFile, readdir, writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
import type { Script, ScriptCard } from '~/types/script';
export class LocalScriptsService {
private scriptsDirectory: string;
constructor() {
this.scriptsDirectory = join(process.cwd(), 'scripts', 'json');
}
async getJsonFiles(): Promise<string[]> {
try {
const files = await readdir(this.scriptsDirectory);
return files.filter(file => file.endsWith('.json'));
} catch (error) {
console.error('Error reading scripts directory:', error);
throw new Error('Failed to read scripts directory');
}
}
async getScriptContent(filename: string): Promise<Script> {
try {
const filePath = join(this.scriptsDirectory, filename);
const content = await readFile(filePath, 'utf-8');
return JSON.parse(content) as Script;
} catch (error) {
console.error(`Error reading script file ${filename}:`, error);
throw new Error(`Failed to read script: ${filename}`);
}
}
async getAllScripts(): Promise<Script[]> {
try {
const jsonFiles = await this.getJsonFiles();
const scripts: Script[] = [];
for (const filename of jsonFiles) {
try {
const script = await this.getScriptContent(filename);
scripts.push(script);
} catch (error) {
console.error(`Failed to parse script ${filename}:`, error);
// Continue with other files even if one fails
}
}
return scripts;
} catch (error) {
console.error('Error fetching all scripts:', error);
throw new Error('Failed to fetch scripts from local directory');
}
}
async getScriptCards(): Promise<ScriptCard[]> {
try {
const scripts = await this.getAllScripts();
return scripts.map(script => ({
name: script.name,
slug: script.slug,
description: script.description,
logo: script.logo,
type: script.type,
updateable: script.updateable,
website: script.website,
}));
} catch (error) {
console.error('Error creating script cards:', error);
throw new Error('Failed to create script cards');
}
}
async getScriptBySlug(slug: string): Promise<Script | null> {
try {
const scripts = await this.getAllScripts();
return scripts.find(script => script.slug === slug) || null;
} catch (error) {
console.error('Error fetching script by slug:', error);
throw new Error(`Failed to fetch script: ${slug}`);
}
}
async saveScriptsFromGitHub(scripts: Script[]): Promise<void> {
try {
// Ensure the directory exists
await mkdir(this.scriptsDirectory, { recursive: true });
// Save each script as a JSON file
for (const script of scripts) {
const filename = `${script.slug}.json`;
const filePath = join(this.scriptsDirectory, filename);
const content = JSON.stringify(script, null, 2);
await writeFile(filePath, content, 'utf-8');
}
console.log(`Successfully saved ${scripts.length} scripts to ${this.scriptsDirectory}`);
} catch (error) {
console.error('Error saving scripts from GitHub:', error);
throw new Error('Failed to save scripts from GitHub');
}
}
}
// Singleton instance
export const localScriptsService = new LocalScriptsService();