diff --git a/.env.example b/.env.example
index f1963f7..5de04b7 100644
--- a/.env.example
+++ b/.env.example
@@ -18,7 +18,12 @@ ALLOWED_SCRIPT_PATHS="scripts/"
WEBSOCKET_PORT="3001"
# User settings
+# Optional tokens for private repos: GITHUB_TOKEN (GitHub), GITLAB_TOKEN (GitLab),
+# BITBUCKET_APP_PASSWORD or BITBUCKET_TOKEN (Bitbucket). REPO_URL and added repos
+# can be GitHub, GitLab, Bitbucket, or custom Git servers.
GITHUB_TOKEN=
+GITLAB_TOKEN=
+BITBUCKET_APP_PASSWORD=
SAVE_FILTER=false
FILTERS=
AUTH_USERNAME=
diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx
index fbeb1a7..a9146de 100644
--- a/src/app/_components/GeneralSettingsModal.tsx
+++ b/src/app/_components/GeneralSettingsModal.tsx
@@ -1617,7 +1617,7 @@ export function GeneralSettingsModal({
) =>
setNewRepoUrl(e.target.value)
@@ -1626,8 +1626,9 @@ export function GeneralSettingsModal({
className="w-full"
/>
- Enter a GitHub repository URL (e.g.,
- https://github.com/owner/repo)
+ Supported: GitHub, GitLab, Bitbucket, or custom Git
+ servers (e.g. https://github.com/owner/repo,
+ https://gitlab.com/owner/repo)
diff --git a/src/env.js b/src/env.js
index cb4aae6..6b6972f 100644
--- a/src/env.js
+++ b/src/env.js
@@ -23,8 +23,11 @@ export const env = createEnv({
ALLOWED_SCRIPT_PATHS: z.string().default("scripts/"),
// WebSocket Configuration
WEBSOCKET_PORT: z.string().default("3001"),
- // GitHub Configuration
+ // Git provider tokens (optional, for private repos)
GITHUB_TOKEN: z.string().optional(),
+ GITLAB_TOKEN: z.string().optional(),
+ BITBUCKET_APP_PASSWORD: z.string().optional(),
+ BITBUCKET_TOKEN: z.string().optional(),
// Authentication Configuration
AUTH_USERNAME: z.string().optional(),
AUTH_PASSWORD_HASH: z.string().optional(),
@@ -62,8 +65,10 @@ export const env = createEnv({
ALLOWED_SCRIPT_PATHS: process.env.ALLOWED_SCRIPT_PATHS,
// WebSocket Configuration
WEBSOCKET_PORT: process.env.WEBSOCKET_PORT,
- // GitHub Configuration
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
+ GITLAB_TOKEN: process.env.GITLAB_TOKEN,
+ BITBUCKET_APP_PASSWORD: process.env.BITBUCKET_APP_PASSWORD,
+ BITBUCKET_TOKEN: process.env.BITBUCKET_TOKEN,
// Authentication Configuration
AUTH_USERNAME: process.env.AUTH_USERNAME,
AUTH_PASSWORD_HASH: process.env.AUTH_PASSWORD_HASH,
diff --git a/src/server/lib/gitProvider/bitbucket.ts b/src/server/lib/gitProvider/bitbucket.ts
new file mode 100644
index 0000000..4205c70
--- /dev/null
+++ b/src/server/lib/gitProvider/bitbucket.ts
@@ -0,0 +1,55 @@
+import type { DirEntry, GitProvider } from './types';
+import { parseRepoUrl } from '../repositoryUrlValidation';
+
+export class BitbucketProvider implements GitProvider {
+ async listDirectory(repoUrl: string, path: string, branch: string): Promise {
+ const { owner, repo } = parseRepoUrl(repoUrl);
+ const listUrl = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/src/${encodeURIComponent(branch)}/${path}`;
+ const headers: Record = {
+ 'User-Agent': 'PVEScripts-Local/1.0',
+ };
+ const token = process.env.BITBUCKET_APP_PASSWORD ?? process.env.BITBUCKET_TOKEN;
+ if (token) {
+ const auth = Buffer.from(`:${token}`).toString('base64');
+ headers.Authorization = `Basic ${auth}`;
+ }
+
+ const response = await fetch(listUrl, { headers });
+ if (!response.ok) {
+ throw new Error(`Bitbucket API error: ${response.status} ${response.statusText}`);
+ }
+
+ const body = (await response.json()) as { values?: { path: string; type: string }[] };
+ const data = body.values ?? (Array.isArray(body) ? body : []);
+ if (!Array.isArray(data)) {
+ throw new Error('Bitbucket API returned unexpected response');
+ }
+ return data.map((item: { path: string; type: string }) => {
+ const name = item.path.split('/').pop() ?? item.path;
+ return {
+ name,
+ path: item.path,
+ type: item.type === 'commit_directory' ? ('dir' as const) : ('file' as const),
+ };
+ });
+ }
+
+ async downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise {
+ const { owner, repo } = parseRepoUrl(repoUrl);
+ const rawUrl = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/src/${encodeURIComponent(branch)}/${filePath}`;
+ const headers: Record = {
+ 'User-Agent': 'PVEScripts-Local/1.0',
+ };
+ const token = process.env.BITBUCKET_APP_PASSWORD ?? process.env.BITBUCKET_TOKEN;
+ if (token) {
+ const auth = Buffer.from(`:${token}`).toString('base64');
+ headers.Authorization = `Basic ${auth}`;
+ }
+
+ const response = await fetch(rawUrl, { headers });
+ if (!response.ok) {
+ throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
+ }
+ return response.text();
+ }
+}
diff --git a/src/server/lib/gitProvider/custom.ts b/src/server/lib/gitProvider/custom.ts
new file mode 100644
index 0000000..a4db2a0
--- /dev/null
+++ b/src/server/lib/gitProvider/custom.ts
@@ -0,0 +1,44 @@
+import type { DirEntry, GitProvider } from "./types";
+import { parseRepoUrl } from "../repositoryUrlValidation";
+
+export class CustomProvider implements GitProvider {
+ async listDirectory(repoUrl: string, path: string, branch: string): Promise {
+ const { origin, owner, repo } = parseRepoUrl(repoUrl);
+ const apiUrl = `${origin}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`;
+ const headers: Record = { "User-Agent": "PVEScripts-Local/1.0" };
+ const token = process.env.GITEA_TOKEN ?? process.env.GIT_TOKEN;
+ if (token) headers.Authorization = `token ${token}`;
+
+ const response = await fetch(apiUrl, { headers });
+ if (!response.ok) {
+ throw new Error(`Custom Git server: list directory failed (${response.status}).`);
+ }
+ const data = (await response.json()) as { type: string; name: string; path: string }[];
+ if (!Array.isArray(data)) {
+ const single = data as unknown as { type?: string; name?: string; path?: string };
+ if (single?.name) {
+ return [{ name: single.name, path: single.path ?? path, type: single.type === "dir" ? "dir" : "file" }];
+ }
+ throw new Error("Custom Git server returned unexpected response");
+ }
+ return data.map((item) => ({
+ name: item.name,
+ path: item.path,
+ type: item.type === "dir" ? ("dir" as const) : ("file" as const),
+ }));
+ }
+
+ async downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise {
+ const { origin, owner, repo } = parseRepoUrl(repoUrl);
+ const rawUrl = `${origin}/${owner}/${repo}/raw/${encodeURIComponent(branch)}/${filePath}`;
+ const headers: Record = { "User-Agent": "PVEScripts-Local/1.0" };
+ const token = process.env.GITEA_TOKEN ?? process.env.GIT_TOKEN;
+ if (token) headers.Authorization = `token ${token}`;
+
+ const response = await fetch(rawUrl, { headers });
+ if (!response.ok) {
+ throw new Error(`Failed to download ${filePath} from custom Git server (${response.status}).`);
+ }
+ return response.text();
+ }
+}
diff --git a/src/server/lib/gitProvider/github.ts b/src/server/lib/gitProvider/github.ts
new file mode 100644
index 0000000..5a08aaa
--- /dev/null
+++ b/src/server/lib/gitProvider/github.ts
@@ -0,0 +1,60 @@
+import type { DirEntry, GitProvider } from './types';
+import { parseRepoUrl } from '../repositoryUrlValidation';
+
+export class GitHubProvider implements GitProvider {
+ async listDirectory(repoUrl: string, path: string, branch: string): Promise {
+ const { owner, repo } = parseRepoUrl(repoUrl);
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`;
+ const headers: Record = {
+ Accept: 'application/vnd.github.v3+json',
+ 'User-Agent': 'PVEScripts-Local/1.0',
+ };
+ const token = process.env.GITHUB_TOKEN;
+ if (token) headers.Authorization = `token ${token}`;
+
+ const response = await fetch(apiUrl, { headers });
+ if (!response.ok) {
+ if (response.status === 403) {
+ const err = new Error(
+ `GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN. Status: ${response.status} ${response.statusText}`
+ );
+ (err as Error & { name: string }).name = 'RateLimitError';
+ throw err;
+ }
+ throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
+ }
+
+ const data = (await response.json()) as { type: string; name: string; path: string }[];
+ if (!Array.isArray(data)) {
+ throw new Error('GitHub API returned unexpected response');
+ }
+ return data.map((item) => ({
+ name: item.name,
+ path: item.path,
+ type: item.type === 'dir' ? ('dir' as const) : ('file' as const),
+ }));
+ }
+
+ async downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise {
+ const { owner, repo } = parseRepoUrl(repoUrl);
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${encodeURIComponent(branch)}/${filePath}`;
+ const headers: Record = {
+ 'User-Agent': 'PVEScripts-Local/1.0',
+ };
+ const token = process.env.GITHUB_TOKEN;
+ if (token) headers.Authorization = `token ${token}`;
+
+ const response = await fetch(rawUrl, { headers });
+ if (!response.ok) {
+ if (response.status === 403) {
+ const err = new Error(
+ `GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN.`
+ );
+ (err as Error & { name: string }).name = 'RateLimitError';
+ throw err;
+ }
+ throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
+ }
+ return response.text();
+ }
+}
diff --git a/src/server/lib/gitProvider/gitlab.ts b/src/server/lib/gitProvider/gitlab.ts
new file mode 100644
index 0000000..089b3c5
--- /dev/null
+++ b/src/server/lib/gitProvider/gitlab.ts
@@ -0,0 +1,58 @@
+import type { DirEntry, GitProvider } from './types';
+import { parseRepoUrl } from '../repositoryUrlValidation';
+
+export class GitLabProvider implements GitProvider {
+ private getBaseUrl(repoUrl: string): string {
+ const { origin } = parseRepoUrl(repoUrl);
+ return origin;
+ }
+
+ private getProjectId(repoUrl: string): string {
+ const { owner, repo } = parseRepoUrl(repoUrl);
+ return encodeURIComponent(`${owner}/${repo}`);
+ }
+
+ async listDirectory(repoUrl: string, path: string, branch: string): Promise {
+ const baseUrl = this.getBaseUrl(repoUrl);
+ const projectId = this.getProjectId(repoUrl);
+ const apiUrl = `${baseUrl}/api/v4/projects/${projectId}/repository/tree?path=${encodeURIComponent(path)}&ref=${encodeURIComponent(branch)}&per_page=100`;
+ const headers: Record = {
+ 'User-Agent': 'PVEScripts-Local/1.0',
+ };
+ const token = process.env.GITLAB_TOKEN;
+ if (token) headers['PRIVATE-TOKEN'] = token;
+
+ const response = await fetch(apiUrl, { headers });
+ if (!response.ok) {
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}`);
+ }
+
+ const data = (await response.json()) as { type: string; name: string; path: string }[];
+ if (!Array.isArray(data)) {
+ throw new Error('GitLab API returned unexpected response');
+ }
+ return data.map((item) => ({
+ name: item.name,
+ path: item.path,
+ type: item.type === 'tree' ? ('dir' as const) : ('file' as const),
+ }));
+ }
+
+ async downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise {
+ const baseUrl = this.getBaseUrl(repoUrl);
+ const projectId = this.getProjectId(repoUrl);
+ const encodedPath = encodeURIComponent(filePath);
+ const rawUrl = `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(branch)}`;
+ const headers: Record = {
+ 'User-Agent': 'PVEScripts-Local/1.0',
+ };
+ const token = process.env.GITLAB_TOKEN;
+ if (token) headers['PRIVATE-TOKEN'] = token;
+
+ const response = await fetch(rawUrl, { headers });
+ if (!response.ok) {
+ throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
+ }
+ return response.text();
+ }
+}
diff --git a/src/server/lib/gitProvider/index.js b/src/server/lib/gitProvider/index.js
new file mode 100644
index 0000000..24c6c60
--- /dev/null
+++ b/src/server/lib/gitProvider/index.js
@@ -0,0 +1 @@
+export { listDirectory, downloadRawFile, getRepoProvider } from "./index.ts";
diff --git a/src/server/lib/gitProvider/index.ts b/src/server/lib/gitProvider/index.ts
new file mode 100644
index 0000000..493e70b
--- /dev/null
+++ b/src/server/lib/gitProvider/index.ts
@@ -0,0 +1,28 @@
+import type { DirEntry, GitProvider } from "./types";
+import { getRepoProvider } from "../repositoryUrlValidation";
+import { GitHubProvider } from "./github";
+import { GitLabProvider } from "./gitlab";
+import { BitbucketProvider } from "./bitbucket";
+import { CustomProvider } from "./custom";
+
+const providers: Record = {
+ github: new GitHubProvider(),
+ gitlab: new GitLabProvider(),
+ bitbucket: new BitbucketProvider(),
+ custom: new CustomProvider(),
+};
+
+export type { DirEntry, GitProvider };
+export { getRepoProvider };
+
+export function getGitProvider(repoUrl: string): GitProvider {
+ return providers[getRepoProvider(repoUrl)]!;
+}
+
+export async function listDirectory(repoUrl: string, path: string, branch: string): Promise {
+ return getGitProvider(repoUrl).listDirectory(repoUrl, path, branch);
+}
+
+export async function downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise {
+ return getGitProvider(repoUrl).downloadRawFile(repoUrl, filePath, branch);
+}
diff --git a/src/server/lib/gitProvider/types.ts b/src/server/lib/gitProvider/types.ts
new file mode 100644
index 0000000..7d16df5
--- /dev/null
+++ b/src/server/lib/gitProvider/types.ts
@@ -0,0 +1,14 @@
+/**
+ * Git provider interface for listing and downloading repository files.
+ */
+
+export type DirEntry = {
+ name: string;
+ path: string;
+ type: 'file' | 'dir';
+};
+
+export interface GitProvider {
+ listDirectory(repoUrl: string, path: string, branch: string): Promise;
+ downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise;
+}
diff --git a/src/server/lib/repositoryUrlValidation.js b/src/server/lib/repositoryUrlValidation.js
new file mode 100644
index 0000000..f145fa6
--- /dev/null
+++ b/src/server/lib/repositoryUrlValidation.js
@@ -0,0 +1,37 @@
+/**
+ * Repository URL validation (JS mirror for server.js).
+ */
+const VALID_REPO_URL =
+ /^(https?:\/\/)(github\.com|gitlab\.com|bitbucket\.org|[^/]+)\/[^/]+\/[^/]+$/;
+
+export const REPO_URL_ERROR_MESSAGE =
+ 'Invalid repository URL. Supported: GitHub, GitLab, Bitbucket, and custom Git servers (e.g. https://host/owner/repo).';
+
+export function isValidRepositoryUrl(url) {
+ if (typeof url !== 'string' || !url.trim()) return false;
+ return VALID_REPO_URL.test(url.trim());
+}
+
+export function getRepoProvider(url) {
+ if (!isValidRepositoryUrl(url)) throw new Error(REPO_URL_ERROR_MESSAGE);
+ const normalized = url.trim().toLowerCase();
+ if (normalized.includes('github.com')) return 'github';
+ if (normalized.includes('gitlab.com')) return 'gitlab';
+ if (normalized.includes('bitbucket.org')) return 'bitbucket';
+ return 'custom';
+}
+
+export function parseRepoUrl(url) {
+ if (!isValidRepositoryUrl(url)) throw new Error(REPO_URL_ERROR_MESSAGE);
+ try {
+ const u = new URL(url.trim());
+ const pathParts = u.pathname.replace(/^\/+/, '').replace(/\.git\/?$/, '').split('/');
+ return {
+ origin: u.origin,
+ owner: pathParts[0] ?? '',
+ repo: pathParts[1] ?? '',
+ };
+ } catch {
+ throw new Error(REPO_URL_ERROR_MESSAGE);
+ }
+}
diff --git a/src/server/lib/repositoryUrlValidation.ts b/src/server/lib/repositoryUrlValidation.ts
new file mode 100644
index 0000000..36479b5
--- /dev/null
+++ b/src/server/lib/repositoryUrlValidation.ts
@@ -0,0 +1,57 @@
+/**
+ * Repository URL validation and provider detection.
+ * Supports GitHub, GitLab, Bitbucket, and custom Git servers.
+ */
+
+const VALID_REPO_URL =
+ /^(https?:\/\/)(github\.com|gitlab\.com|bitbucket\.org|[^/]+)\/[^/]+\/[^/]+$/;
+
+export const REPO_URL_ERROR_MESSAGE =
+ 'Invalid repository URL. Supported: GitHub, GitLab, Bitbucket, and custom Git servers (e.g. https://host/owner/repo).';
+
+export type RepoProvider = 'github' | 'gitlab' | 'bitbucket' | 'custom';
+
+/**
+ * Check if a string is a valid repository URL (format only).
+ */
+export function isValidRepositoryUrl(url: string): boolean {
+ if (typeof url !== 'string' || !url.trim()) return false;
+ return VALID_REPO_URL.test(url.trim());
+}
+
+/**
+ * Detect the Git provider from a repository URL.
+ */
+export function getRepoProvider(url: string): RepoProvider {
+ if (!isValidRepositoryUrl(url)) {
+ throw new Error(REPO_URL_ERROR_MESSAGE);
+ }
+ const normalized = url.trim().toLowerCase();
+ if (normalized.includes('github.com')) return 'github';
+ if (normalized.includes('gitlab.com')) return 'gitlab';
+ if (normalized.includes('bitbucket.org')) return 'bitbucket';
+ return 'custom';
+}
+
+/**
+ * Parse owner and repo from a repository URL (path segments).
+ * Works for GitHub, GitLab, Bitbucket, and custom (host/owner/repo).
+ */
+export function parseRepoUrl(url: string): { origin: string; owner: string; repo: string } {
+ if (!isValidRepositoryUrl(url)) {
+ throw new Error(REPO_URL_ERROR_MESSAGE);
+ }
+ try {
+ const u = new URL(url.trim());
+ const pathParts = u.pathname.replace(/^\/+/, '').replace(/\.git\/?$/, '').split('/');
+ const owner = pathParts[0] ?? '';
+ const repo = pathParts[1] ?? '';
+ return {
+ origin: u.origin,
+ owner,
+ repo,
+ };
+ } catch {
+ throw new Error(REPO_URL_ERROR_MESSAGE);
+ }
+}
diff --git a/src/server/services/githubJsonService.js b/src/server/services/githubJsonService.js
index da5f53b..519d526 100644
--- a/src/server/services/githubJsonService.js
+++ b/src/server/services/githubJsonService.js
@@ -2,6 +2,7 @@
import { writeFile, mkdir, readdir, readFile } from 'fs/promises';
import { join } from 'path';
import { repositoryService } from './repositoryService.js';
+import { listDirectory, downloadRawFile } from '../lib/gitProvider/index.js';
// Get environment variables
const getEnv = () => ({
@@ -28,76 +29,9 @@ class GitHubJsonService {
}
}
- getBaseUrl(repoUrl) {
- const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
- if (!urlMatch) {
- throw new Error(`Invalid GitHub repository URL: ${repoUrl}`);
- }
-
- const [, owner, repo] = urlMatch;
- return `https://api.github.com/repos/${owner}/${repo}`;
- }
-
- extractRepoPath(repoUrl) {
- const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
- if (!match) {
- throw new Error('Invalid GitHub repository URL');
- }
- return `${match[1]}/${match[2]}`;
- }
-
- async fetchFromGitHub(repoUrl, endpoint) {
- const baseUrl = this.getBaseUrl(repoUrl);
- const env = getEnv();
-
- const headers = {
- 'Accept': 'application/vnd.github.v3+json',
- 'User-Agent': 'PVEScripts-Local/1.0',
- };
-
- if (env.GITHUB_TOKEN) {
- headers.Authorization = `token ${env.GITHUB_TOKEN}`;
- }
-
- const response = await fetch(`${baseUrl}${endpoint}`, { headers });
-
- if (!response.ok) {
- if (response.status === 403) {
- const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
- error.name = 'RateLimitError';
- throw error;
- }
- throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
- }
-
- return response.json();
- }
-
async downloadJsonFile(repoUrl, filePath) {
this.initializeConfig();
- const repoPath = this.extractRepoPath(repoUrl);
- const rawUrl = `https://raw.githubusercontent.com/${repoPath}/${this.branch}/${filePath}`;
- const env = getEnv();
-
- const headers = {
- 'User-Agent': 'PVEScripts-Local/1.0',
- };
-
- if (env.GITHUB_TOKEN) {
- headers.Authorization = `token ${env.GITHUB_TOKEN}`;
- }
-
- const response = await fetch(rawUrl, { headers });
- if (!response.ok) {
- if (response.status === 403) {
- const error = new Error(`GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN for higher limits.`);
- error.name = 'RateLimitError';
- throw error;
- }
- throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
- }
-
- const content = await response.text();
+ const content = await downloadRawFile(repoUrl, filePath, this.branch);
const script = JSON.parse(content);
script.repository_url = repoUrl;
return script;
@@ -105,16 +39,13 @@ class GitHubJsonService {
async getJsonFiles(repoUrl) {
this.initializeConfig();
-
try {
- const files = await this.fetchFromGitHub(
- repoUrl,
- `/contents/${this.jsonFolder}?ref=${this.branch}`
- );
-
- return files.filter(file => file.name.endsWith('.json'));
+ const entries = await listDirectory(repoUrl, this.jsonFolder, this.branch);
+ return entries
+ .filter((e) => e.type === 'file' && e.name.endsWith('.json'))
+ .map((e) => ({ name: e.name, path: e.path }));
} catch (error) {
- console.error(`Error fetching JSON files from GitHub (${repoUrl}):`, error);
+ console.error(`Error fetching JSON files from repository (${repoUrl}):`, error);
throw new Error(`Failed to fetch script files from repository: ${repoUrl}`);
}
}
diff --git a/src/server/services/githubJsonService.ts b/src/server/services/githubJsonService.ts
index b70b3d6..64da8a7 100644
--- a/src/server/services/githubJsonService.ts
+++ b/src/server/services/githubJsonService.ts
@@ -3,6 +3,7 @@ import { join } from 'path';
import { env } from '../../env.js';
import type { Script, ScriptCard, GitHubFile } from '../../types/script';
import { repositoryService } from './repositoryService';
+import { listDirectory, downloadRawFile } from '~/server/lib/gitProvider';
export class GitHubJsonService {
private branch: string | null = null;
@@ -22,96 +23,24 @@ export class GitHubJsonService {
}
}
- private getBaseUrl(repoUrl: string): string {
- const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
- if (!urlMatch) {
- throw new Error(`Invalid GitHub repository URL: ${repoUrl}`);
- }
-
- const [, owner, repo] = urlMatch;
- return `https://api.github.com/repos/${owner}/${repo}`;
- }
-
- private extractRepoPath(repoUrl: string): string {
- const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
- if (!match) {
- throw new Error('Invalid GitHub repository URL');
- }
- return `${match[1]}/${match[2]}`;
- }
-
- private async fetchFromGitHub(repoUrl: string, endpoint: string): Promise {
- const baseUrl = this.getBaseUrl(repoUrl);
-
- const headers: HeadersInit = {
- 'Accept': 'application/vnd.github.v3+json',
- 'User-Agent': 'PVEScripts-Local/1.0',
- };
-
- // Add GitHub token authentication if available
- if (env.GITHUB_TOKEN) {
- headers.Authorization = `token ${env.GITHUB_TOKEN}`;
- }
-
- const response = await fetch(`${baseUrl}${endpoint}`, { headers });
-
- if (!response.ok) {
- if (response.status === 403) {
- const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
- error.name = 'RateLimitError';
- throw error;
- }
- throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
- }
-
- const data = await response.json();
- return data as T;
- }
-
private async downloadJsonFile(repoUrl: string, filePath: string): Promise