first commit

This commit is contained in:
2026-08-08 11:08:00 -05:00
commit c51740c589
32 changed files with 2461 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
import { sdk } from '../sdk'
import { storeJson } from '../fileModels/store.json'
import { i18n } from '../i18n'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
igdbClientId: Value.text({
name: i18n('IGDB Client ID'),
description: i18n('Optional Twitch application client ID used by IGDB.'),
required: false,
default: null,
}),
igdbClientSecret: Value.text({
name: i18n('IGDB Client Secret'),
description: i18n('Optional Twitch application secret used by IGDB.'),
required: false,
masked: true,
default: null,
}),
mobygamesApiKey: Value.text({
name: i18n('MobyGames API Key'),
description: i18n('Optional API key used to retrieve MobyGames metadata.'),
required: false,
masked: true,
default: null,
}),
steamGridDbApiKey: Value.text({
name: i18n('SteamGridDB API Key'),
description: i18n('Optional API key used to retrieve SteamGridDB artwork.'),
required: false,
masked: true,
default: null,
}),
})
export const configure = sdk.Action.withInput(
'configure',
async () => ({
name: i18n('Configure Metadata Providers'),
description: i18n(
'Save optional API credentials supported by RomM 3.5.0.',
),
warning: i18n(
'Saved values are passed to RomM after the service is restarted. Leave a field blank to remove it.',
),
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
inputSpec,
async () => ({
igdbClientId: null,
igdbClientSecret: null,
mobygamesApiKey: null,
steamGridDbApiKey: null,
}),
async ({ effects, input }) => {
await storeJson.merge(effects, {
igdbClientId: input.igdbClientId ?? '',
igdbClientSecret: input.igdbClientSecret ?? '',
mobygamesApiKey: input.mobygamesApiKey ?? '',
steamGridDbApiKey: input.steamGridDbApiKey ?? '',
})
return {
version: '1',
title: i18n('Configuration Saved'),
message: i18n(
'Restart RomM to apply the metadata provider configuration.',
),
result: null,
}
},
)
+4
View File
@@ -0,0 +1,4 @@
import { sdk } from '../sdk'
import { configure } from './configure'
export const actions = sdk.Actions.of().addAction(configure)
+23
View File
@@ -0,0 +1,23 @@
import { sdk } from './sdk'
import { storeJson } from './fileModels/store.json'
import { databaseName, databaseUser } from './utils'
export const { createBackup, restoreInit } = sdk.setupBackups(
async ({ effects }) =>
sdk.Backups.withMysqlDump({
imageId: 'mariadb',
dbVolume: 'database',
datadir: '/var/lib/mysql',
database: databaseName,
user: databaseUser,
password: async () => {
const store = await storeJson.read().once()
if (!store?.databasePassword) {
throw new Error('RomM database password is missing')
}
return store.databasePassword
},
engine: 'mariadb',
readyTimeout: 120000,
}).addVolume('main'),
)
+3
View File
@@ -0,0 +1,3 @@
import { sdk } from './sdk'
export const setDependencies = sdk.setupDependencies(async () => ({}))
+19
View File
@@ -0,0 +1,19 @@
import { FileHelper, z } from '@start9labs/start-sdk'
import { sdk } from '../sdk'
const shape = z
.object({
databaseRootPassword: z.string().catch(''),
databasePassword: z.string().catch(''),
authSecret: z.string().catch(''),
igdbClientId: z.string().catch(''),
igdbClientSecret: z.string().catch(''),
mobygamesApiKey: z.string().catch(''),
steamGridDbApiKey: z.string().catch(''),
})
.strip()
export const storeJson = FileHelper.json(
{ base: sdk.volumes.main, subpath: './store.json' },
shape,
)
+29
View File
@@ -0,0 +1,29 @@
export const DEFAULT_LANG = 'en_US'
const dict = {
Database: 0,
'MariaDB is ready': 1,
'MariaDB is not ready': 2,
'Web Interface': 3,
'RomM is ready': 4,
'RomM is not ready': 5,
'RomM Web Interface': 6,
'Browse, scan, and manage your game library': 7,
'IGDB Client ID': 8,
'Optional Twitch application client ID used by IGDB.': 9,
'IGDB Client Secret': 10,
'Optional Twitch application secret used by IGDB.': 11,
'MobyGames API Key': 12,
'Optional API key used to retrieve MobyGames metadata.': 13,
'SteamGridDB API Key': 14,
'Optional API key used to retrieve SteamGridDB artwork.': 15,
'Configure Metadata Providers': 16,
'Save optional API credentials supported by RomM 3.5.0.': 17,
'Saved values are passed to RomM after the service is restarted. Leave a field blank to remove it.': 18,
'Configuration Saved': 19,
'Restart RomM to apply the metadata provider configuration.': 20,
} as const
export type I18nKey = keyof typeof dict
export type LangDict = Record<(typeof dict)[I18nKey], string>
export default dict
@@ -0,0 +1,3 @@
import { LangDict } from './default'
export default {} satisfies Record<string, LangDict>
+5
View File
@@ -0,0 +1,5 @@
import { setupI18n } from '@start9labs/start-sdk'
import defaultDict, { DEFAULT_LANG } from './dictionaries/default'
import translations from './dictionaries/translations'
export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG)
+10
View File
@@ -0,0 +1,10 @@
export { createBackup } from './backups'
export { main } from './main'
export { init, uninit } from './init'
export { actions } from './actions'
import { buildManifest } from '@start9labs/start-sdk'
import { manifest as sdkManifest } from './manifest'
import { versionGraph } from './versions'
export const manifest = buildManifest(versionGraph, sdkManifest)
+18
View File
@@ -0,0 +1,18 @@
import { actions } from '../actions'
import { restoreInit } from '../backups'
import { setDependencies } from '../dependencies'
import { setInterfaces } from '../interfaces'
import { sdk } from '../sdk'
import { versionGraph } from '../versions'
import { seedStore } from './seedStore'
export const init = sdk.setupInit(
restoreInit,
versionGraph,
seedStore,
setInterfaces,
setDependencies,
actions,
)
export const uninit = sdk.setupUninit(versionGraph)
+16
View File
@@ -0,0 +1,16 @@
import { utils } from '@start9labs/start-sdk'
import { storeJson } from '../fileModels/store.json'
import { sdk } from '../sdk'
const secret = () =>
utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 64 })
export const seedStore = sdk.setupOnInit(async (effects, kind) => {
if (kind !== 'install') return
await storeJson.merge(effects, {
databaseRootPassword: secret(),
databasePassword: secret(),
authSecret: secret(),
})
})
+24
View File
@@ -0,0 +1,24 @@
import { sdk } from './sdk'
import { i18n } from './i18n'
import { uiHostId, uiPort } from './utils'
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const host = sdk.MultiHost.of(effects, uiHostId)
const origin = await host.bindPort(uiPort, {
protocol: 'http',
preferredExternalPort: 80,
})
const ui = sdk.createInterface(effects, {
name: i18n('RomM Web Interface'),
id: 'ui',
description: i18n('Browse, scan, and manage your game library'),
type: 'ui',
masked: false,
schemeOverride: null,
username: null,
path: '',
query: {},
})
return [await origin.export([ui])]
})
+124
View File
@@ -0,0 +1,124 @@
import { sdk } from './sdk'
import { i18n } from './i18n'
import { storeJson } from './fileModels/store.json'
import {
databaseName,
databasePort,
databaseUser,
mainMounts,
uiPort,
} from './utils'
export const main = sdk.setupMain(async ({ effects }) => {
const store = await storeJson.read().const(effects)
if (
!store?.databaseRootPassword ||
!store.databasePassword ||
!store.authSecret
) {
throw new Error('RomM internal secrets have not been initialized')
}
const mariadb = sdk.SubContainer.of(
effects,
{ imageId: 'mariadb' },
sdk.Mounts.of().mountVolume({
volumeId: 'database',
subpath: null,
mountpoint: '/var/lib/mysql',
readonly: false,
}),
'romm-mariadb-sub',
)
const romm = sdk.SubContainer.of(
effects,
{ imageId: 'romm' },
sdk.Mounts.of()
.mountVolume({
volumeId: 'main',
subpath: 'library',
mountpoint: mainMounts.library,
readonly: false,
})
.mountVolume({
volumeId: 'main',
subpath: 'resources',
mountpoint: mainMounts.resources,
readonly: false,
})
.mountVolume({
volumeId: 'main',
subpath: 'assets',
mountpoint: mainMounts.assets,
readonly: false,
})
.mountVolume({
volumeId: 'main',
subpath: 'config',
mountpoint: mainMounts.config,
readonly: false,
})
.mountVolume({
volumeId: 'main',
subpath: 'redis-data',
mountpoint: mainMounts.redis,
readonly: false,
}),
'romm-app-sub',
)
return sdk.Daemons.of(effects)
.addDaemon('mariadb', {
subcontainer: mariadb,
exec: {
command: sdk.useEntrypoint(),
runAsInit: true,
env: {
MARIADB_ROOT_PASSWORD: store.databaseRootPassword,
MARIADB_DATABASE: databaseName,
MARIADB_USER: databaseUser,
MARIADB_PASSWORD: store.databasePassword,
},
},
ready: {
display: i18n('Database'),
gracePeriod: 120000,
fn: () =>
sdk.healthCheck.checkPortListening(effects, databasePort, {
successMessage: i18n('MariaDB is ready'),
errorMessage: i18n('MariaDB is not ready'),
}),
},
requires: [],
})
.addDaemon('romm', {
subcontainer: romm,
exec: {
command: sdk.useEntrypoint(),
runAsInit: true,
env: {
DB_HOST: '127.0.0.1',
DB_PORT: String(databasePort),
DB_NAME: databaseName,
DB_USER: databaseUser,
DB_PASSWD: store.databasePassword,
ROMM_AUTH_SECRET_KEY: store.authSecret,
IGDB_CLIENT_ID: store.igdbClientId,
IGDB_CLIENT_SECRET: store.igdbClientSecret,
MOBYGAMES_API_KEY: store.mobygamesApiKey,
STEAMGRIDDB_API_KEY: store.steamGridDbApiKey,
},
},
ready: {
display: i18n('Web Interface'),
gracePeriod: 180000,
fn: () =>
sdk.healthCheck.checkPortListening(effects, uiPort, {
successMessage: i18n('RomM is ready'),
errorMessage: i18n('RomM is not ready'),
}),
},
requires: ['mariadb'],
})
})
+20
View File
@@ -0,0 +1,20 @@
export const short = {
en_US: 'A self-hosted ROM manager for browsing and organizing game libraries',
es_ES: 'Un gestor de ROM autoalojado para explorar y organizar bibliotecas de juegos',
de_DE: 'Ein selbst gehosteter ROM-Manager zum Durchsuchen und Organisieren von Spielesammlungen',
pl_PL: 'Samodzielnie hostowany menedżer ROM-ów do przeglądania i organizowania bibliotek gier',
fr_FR: 'Un gestionnaire de ROM auto-hébergé pour parcourir et organiser les ludothèques',
}
export const long = {
en_US:
'RomM scans, enriches, browses, and manages a personal game library through a responsive web interface. This package runs RomM with a private MariaDB database and persistent library storage.',
es_ES:
'RomM analiza, enriquece, explora y administra una biblioteca personal de juegos mediante una interfaz web adaptable. Este paquete ejecuta RomM con una base de datos MariaDB privada y almacenamiento persistente.',
de_DE:
'RomM scannt, ergänzt, durchsucht und verwaltet eine persönliche Spielesammlung über eine responsive Weboberfläche. Dieses Paket betreibt RomM mit einer privaten MariaDB-Datenbank und persistentem Speicher.',
pl_PL:
'RomM skanuje, wzbogaca, przegląda i zarządza osobistą biblioteką gier przez responsywny interfejs WWW. Ten pakiet uruchamia RomM z prywatną bazą MariaDB i trwałym magazynem.',
fr_FR:
'RomM analyse, enrichit, parcourt et gère une ludothèque personnelle dans une interface web adaptative. Ce paquet exécute RomM avec une base MariaDB privée et un stockage persistant.',
}
+31
View File
@@ -0,0 +1,31 @@
import { setupManifest } from '@start9labs/start-sdk'
import { long, short } from './i18n'
export const manifest = setupManifest({
id: 'romm',
title: 'RomM',
license: 'AGPL-3.0',
packageRepo: 'https://github.com/alex/romm-startos',
upstreamRepo: 'https://github.com/rommapp/romm',
marketingUrl: 'https://romm.app/',
donationUrl: null,
description: { short, long },
volumes: ['main', 'database'],
images: {
romm: {
source: {
dockerTag:
'rommapp/romm:3.5.0@sha256:9ff83725e98e5dfc0b871cb88ca378c539fad66b7afcbe6aad562d2b84d5b802',
},
arch: ['x86_64', 'aarch64'],
},
mariadb: {
source: {
dockerTag:
'mariadb:11.4.5@sha256:49117dcc565cf51aa57ac5fca59ab31213402ff0eae6ffc13c46a37b938f7e4b',
},
arch: ['x86_64', 'aarch64'],
},
},
dependencies: {},
})
+4
View File
@@ -0,0 +1,4 @@
import { StartSdk } from '@start9labs/start-sdk'
import { manifest } from './manifest'
export const sdk = StartSdk.of().withManifest(manifest).build(true)
+13
View File
@@ -0,0 +1,13 @@
export const uiPort = 8080
export const databasePort = 3306
export const uiHostId = 'ui'
export const databaseName = 'romm'
export const databaseUser = 'romm'
export const mainMounts = {
library: '/romm/library',
resources: '/romm/resources',
assets: '/romm/assets',
config: '/romm/config',
redis: '/redis-data',
} as const
+16
View File
@@ -0,0 +1,16 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
export const current = VersionInfo.of({
version: '3.5.0:0',
releaseNotes: {
en_US: 'Initial native StartOS 0.4 package for RomM 3.5.0.',
es_ES: 'Paquete nativo inicial de StartOS 0.4 para RomM 3.5.0.',
de_DE: 'Erstes natives StartOS-0.4-Paket für RomM 3.5.0.',
pl_PL: 'Pierwszy natywny pakiet StartOS 0.4 dla RomM 3.5.0.',
fr_FR: 'Premier paquet StartOS 0.4 natif pour RomM 3.5.0.',
},
migrations: {
up: async () => {},
down: IMPOSSIBLE,
},
})
+4
View File
@@ -0,0 +1,4 @@
import { VersionGraph } from '@start9labs/start-sdk'
import { current } from './current'
export const versionGraph = VersionGraph.of({ current, other: [] })