Phase5
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 9s
CI / Go — edge-gateway build (push) Successful in 13s

This commit is contained in:
smueller
2026-06-26 13:00:01 +02:00
parent 4262464cd5
commit d29a02f2a4
93 changed files with 1875 additions and 55 deletions

View File

@@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest';
import {
isVersionCompatible,
pickPrimaryFile,
resolveInstallDirectory,
resolveModrinthLoader,
resolveProjectType,
} from './compatibility';
import type { ModrinthVersion } from './modrinth';
describe('catalog compatibility', () => {
it('maps paper to plugin installs', () => {
expect(resolveModrinthLoader('PAPER')).toBe('paper');
expect(resolveProjectType('PAPER')).toBe('plugin');
expect(resolveInstallDirectory('plugin')).toBe('plugins');
});
it('maps fabric to mod installs', () => {
expect(resolveModrinthLoader('FABRIC')).toBe('fabric');
expect(resolveProjectType('FABRIC')).toBe('mod');
expect(resolveInstallDirectory('mod')).toBe('mods');
});
it('checks version compatibility', () => {
const version: ModrinthVersion = {
id: 'v1',
project_id: 'p1',
name: '1.0.0',
version_number: '1.0.0',
game_versions: ['1.21.1'],
loaders: ['paper'],
files: [],
date_published: '2026-01-01T00:00:00Z',
};
expect(isVersionCompatible(version, '1.21.1', 'paper')).toBe(true);
expect(isVersionCompatible(version, '1.20.1', 'paper')).toBe(false);
});
it('picks primary jar file', () => {
const version: ModrinthVersion = {
id: 'v1',
project_id: 'p1',
name: '1.0.0',
version_number: '1.0.0',
game_versions: ['1.21.1'],
loaders: ['fabric'],
files: [
{
filename: 'readme.txt',
url: 'https://example.com/readme.txt',
hashes: {},
primary: false,
size: 10,
},
{
filename: 'mod.jar',
url: 'https://example.com/mod.jar',
hashes: { sha512: 'abc' },
primary: true,
size: 100,
},
],
date_published: '2026-01-01T00:00:00Z',
};
expect(pickPrimaryFile(version)?.filename).toBe('mod.jar');
});
});

View File

@@ -0,0 +1,75 @@
import type { ModrinthProjectType, ModrinthVersion, SoftwareFamily } from './modrinth';
export function resolveModrinthLoader(
softwareFamily: SoftwareFamily,
): string | null {
switch (softwareFamily) {
case 'PAPER':
case 'PURPUR':
return 'paper';
case 'FABRIC':
return 'fabric';
case 'FORGE':
return 'forge';
case 'NEOFORGE':
return 'neoforge';
case 'QUILT':
return 'quilt';
default:
return null;
}
}
export function resolveProjectType(
softwareFamily: SoftwareFamily,
): ModrinthProjectType | null {
switch (softwareFamily) {
case 'PAPER':
case 'PURPUR':
return 'plugin';
case 'FABRIC':
case 'FORGE':
case 'NEOFORGE':
case 'QUILT':
return 'mod';
default:
return null;
}
}
export function resolveInstallDirectory(projectType: ModrinthProjectType): string {
switch (projectType) {
case 'plugin':
return 'plugins';
case 'mod':
return 'mods';
case 'modpack':
return 'modpacks';
default:
return 'datapacks';
}
}
export function isVersionCompatible(
version: ModrinthVersion,
gameVersion: string,
loader: string,
): boolean {
const gameMatch = version.game_versions.some(
(value) => value === gameVersion || gameVersion.startsWith(value),
);
const loaderMatch = version.loaders.includes(loader);
return gameMatch && loaderMatch;
}
export function pickPrimaryFile(version: ModrinthVersion): ModrinthVersion['files'][number] | null {
const jarFiles = version.files.filter((file) =>
file.filename.toLowerCase().endsWith('.jar'),
);
if (jarFiles.length === 0) {
return version.files[0] ?? null;
}
return jarFiles.find((file) => file.primary) ?? jarFiles[0] ?? null;
}

View File

@@ -0,0 +1,20 @@
export {
searchModrinthProjects,
getModrinthProject,
getModrinthProjectVersions,
getModrinthVersion,
type ModrinthProject,
type ModrinthSearchResponse,
type ModrinthVersion,
type ModrinthVersionFile,
type ModrinthProjectType,
type SoftwareFamily,
} from './modrinth';
export {
resolveModrinthLoader,
resolveProjectType,
resolveInstallDirectory,
isVersionCompatible,
pickPrimaryFile,
} from './compatibility';

View File

@@ -0,0 +1,148 @@
export type SoftwareFamily =
| 'VANILLA'
| 'PAPER'
| 'PURPUR'
| 'FABRIC'
| 'FORGE'
| 'NEOFORGE'
| 'QUILT'
| 'BEDROCK_DEDICATED';
export type ModrinthProjectType = 'mod' | 'plugin' | 'modpack' | 'resourcepack' | 'shader';
export interface ModrinthProject {
project_id: string;
slug: string;
title: string;
description: string;
project_type: ModrinthProjectType;
downloads: number;
icon_url?: string;
}
export interface ModrinthSearchResponse {
hits: ModrinthProject[];
offset: number;
limit: number;
total_hits: number;
}
export interface ModrinthVersionFile {
hashes: Record<string, string>;
url: string;
filename: string;
primary: boolean;
size: number;
}
export interface ModrinthVersion {
id: string;
project_id: string;
name: string;
version_number: string;
game_versions: string[];
loaders: string[];
files: ModrinthVersionFile[];
date_published: string;
}
const MODRINTH_API = 'https://api.modrinth.com/v2';
export async function searchModrinthProjects(input: {
query: string;
projectType: ModrinthProjectType;
gameVersion: string;
loader?: string;
limit?: number;
}): Promise<ModrinthSearchResponse> {
const facets: string[][] = [
[`project_type:${input.projectType}`],
[`versions:${input.gameVersion}`],
];
if (input.loader) {
facets.push([`categories:${input.loader}`]);
}
const params = new URLSearchParams({
query: input.query,
limit: String(input.limit ?? 20),
index: 'relevance',
facets: JSON.stringify(facets),
});
const response = await fetch(`${MODRINTH_API}/search?${params.toString()}`, {
headers: {
'User-Agent': 'HexaHost-GameCloud/1.0 (catalog-sync)',
},
});
if (!response.ok) {
throw new Error(`Modrinth search failed: ${response.status}`);
}
return response.json() as Promise<ModrinthSearchResponse>;
}
export async function getModrinthProjectVersions(
projectId: string,
): Promise<ModrinthVersion[]> {
const response = await fetch(
`${MODRINTH_API}/project/${encodeURIComponent(projectId)}/version`,
{
headers: {
'User-Agent': 'HexaHost-GameCloud/1.0 (catalog-sync)',
},
},
);
if (!response.ok) {
throw new Error(`Modrinth versions failed: ${response.status}`);
}
return response.json() as Promise<ModrinthVersion[]>;
}
export interface ModrinthProjectDetails {
id: string;
slug: string;
title: string;
description: string;
project_type: ModrinthProjectType;
downloads: number;
icon_url?: string;
}
export async function getModrinthProject(projectId: string): Promise<ModrinthProjectDetails> {
const response = await fetch(
`${MODRINTH_API}/project/${encodeURIComponent(projectId)}`,
{
headers: {
'User-Agent': 'HexaHost-GameCloud/1.0 (catalog-sync)',
},
},
);
if (!response.ok) {
throw new Error(`Modrinth project failed: ${response.status}`);
}
return response.json() as Promise<ModrinthProjectDetails>;
}
export async function getModrinthVersion(versionId: string): Promise<ModrinthVersion> {
const response = await fetch(
`${MODRINTH_API}/version/${encodeURIComponent(versionId)}`,
{
headers: {
'User-Agent': 'HexaHost-GameCloud/1.0 (catalog-sync)',
},
},
);
if (!response.ok) {
throw new Error(`Modrinth version failed: ${response.status}`);
}
return response.json() as Promise<ModrinthVersion>;
}