ogi-addon 2.4.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"main.mjs","names":["pjson.version","z"],"sources":["../package.json","../src/main.ts"],"sourcesContent":["","import ws, { WebSocket } from 'ws';\nimport events from 'node:events';\nimport { ConfigurationBuilder } from './config/ConfigurationBuilder';\nimport type { ConfigurationFile } from './config/ConfigurationBuilder';\nimport { Configuration } from './config/Configuration';\nimport EventResponse from './EventResponse';\nimport type { SearchResult } from './SearchEngine';\nimport Fuse, { IFuseOptions } from 'fuse.js';\n\n/**\n * Exposed events that the programmer can use to listen to and emit events.\n */\nexport type OGIAddonEvent =\n | 'connect'\n | 'disconnect'\n | 'configure'\n | 'authenticate'\n | 'search'\n | 'setup'\n | 'library-search'\n | 'game-details'\n | 'exit'\n | 'check-for-updates'\n | 'request-dl'\n | 'catalog'\n | 'launch-app';\n\n/**\n * The events that the client can send to the server and are handled by the server.\n */\nexport type OGIAddonClientSentEvent =\n | 'response'\n | 'authenticate'\n | 'configure'\n | 'defer-update'\n | 'notification'\n | 'input-asked'\n | 'get-app-details'\n | 'search-app-name'\n | 'flag'\n | 'task-update';\n\n/**\n * The events that the server sends to the client\n * This is the events that the server can send to the client and are handled by the client.\n */\nexport type OGIAddonServerSentEvent =\n | 'authenticate'\n | 'configure'\n | 'config-update'\n | 'launch-app'\n | 'search'\n | 'setup'\n | 'response'\n | 'library-search'\n | 'check-for-updates'\n | 'task-run'\n | 'game-details'\n | 'request-dl'\n | 'catalog';\nexport { ConfigurationBuilder, Configuration, EventResponse };\nexport type { SearchResult };\nconst defaultPort = 7654;\nimport pjson from '../package.json';\nimport { exec, spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport { z } from 'zod';\nexport const VERSION = pjson.version;\n\nexport interface ClientSentEventTypes {\n response: any;\n authenticate: {\n name: string;\n id: string;\n description: string;\n version: string;\n author: string;\n };\n configure: ConfigurationFile;\n 'defer-update': {\n logs: string[];\n progress: number;\n };\n notification: Notification;\n 'input-asked': ConfigurationBuilder<\n Record<string, string | number | boolean>\n >;\n 'task-update': {\n id: string;\n progress: number;\n logs: string[];\n finished: boolean;\n failed: string | undefined;\n };\n 'get-app-details': {\n appID: number;\n storefront: string;\n };\n 'search-app-name': {\n query: string;\n storefront: string;\n };\n flag: {\n flag: string;\n value: string | string[];\n };\n}\n\nexport type BasicLibraryInfo = {\n name: string;\n capsuleImage: string;\n appID: number;\n storefront: string;\n};\n\nexport interface CatalogSection {\n name: string;\n description: string;\n listings: BasicLibraryInfo[];\n}\n\nexport interface CatalogCarouselItem {\n name: string;\n description: string;\n carouselImage: string;\n fullBannerImage?: string;\n appID?: number;\n storefront?: string;\n capsuleImage?: string;\n}\n\nexport interface CatalogWithCarousel {\n sections: Record<string, CatalogSection>;\n carousel?: Record<string, CatalogCarouselItem> | CatalogCarouselItem[];\n}\n\nexport type CatalogResponse =\n | Record<string, CatalogSection>\n | CatalogWithCarousel;\n\n/**\n * UMU ID format: 'steam:${number}' or 'umu:${number}'\n * - steam:${number} → maps to umu-${number} for Steam games\n * - umu:${number} → maps to umu-${number} for non-Steam games\n */\nexport type UmuId = `steam:${number}` | `umu:${number}`;\n\nexport type SetupEventResponse = Omit<\n LibraryInfo,\n | 'capsuleImage'\n | 'coverImage'\n | 'name'\n | 'appID'\n | 'storefront'\n | 'addonsource'\n | 'titleImage'\n> & {\n redistributables?: {\n name: string;\n path: string;\n }[];\n /**\n * UMU Proton integration configuration\n */\n umu?: {\n /**\n * UMU ID for the game. Format: 'steam:${number}' or 'umu:${number}'\n * - steam:${number} → maps to umu-${number} for Steam games\n * - umu:${number} → maps to umu-${number} for non-Steam games\n */\n umuId: UmuId;\n /**\n * Optional DLL overrides. These are relative to the game's cwd.\n * System automatically prepends cwd and sets WINEDLLOVERRIDES=\"dll=n,b\"\n */\n dllOverrides?: string[];\n /**\n * Optional Proton version to use (e.g., 'GE-Proton9-5', 'GE-Proton')\n * If not specified, uses latest UMU-Proton\n */\n protonVersion?: string;\n /**\n * Optional store identifier for protonfixes (e.g., 'gog', 'egs', 'none')\n */\n store?: string;\n /**\n * Cached Steam shortcut app ID after adding UMU game to Steam (avoids re-adding on each launch)\n */\n steamShortcutId?: number;\n };\n};\n\nexport interface EventListenerTypes {\n /**\n * This event is emitted when the addon connects to the OGI Addon Server. Addon does not need to resolve anything.\n * @param event\n * @returns\n */\n connect: (event: EventResponse<void>) => void;\n\n /**\n * This event is emitted when the client requests for the addon to disconnect. Addon does not need to resolve this event, but we recommend `process.exit(0)` so the addon can exit gracefully instead of by force by the addon server.\n * @param reason\n * @returns\n */\n disconnect: (reason: string) => void;\n /**\n * This event is emitted when the client requests for the addon to configure itself. Addon should resolve the event with the internal configuration. (See ConfigurationBuilder)\n * @param config\n * @returns\n */\n configure: (config: ConfigurationBuilder) => ConfigurationBuilder;\n /**\n * This event is called when the client provides a response to any event. This should be treated as middleware.\n * @param response\n * @returns\n */\n response: (response: any) => void;\n\n /**\n * This event is called when the client requests for the addon to authenticate itself. You don't need to provide any info.\n * @param config\n * @returns\n */\n authenticate: (config: any) => void;\n /**\n * This event is emitted when the client requests for a torrent/direct download search to be performed. Addon is given the gameID (could be a steam appID or custom store appID), along with the storefront type. Addon should resolve the event with the search results. (See SearchResult)\n * @param query\n * @param event\n * @returns\n */\n search: (\n query: {\n storefront: string;\n appID: number;\n } & (\n | {\n for: 'game' | 'task' | 'all';\n }\n | {\n for: 'update';\n libraryInfo: LibraryInfo;\n }\n ),\n event: EventResponse<SearchResult[]>\n ) => void;\n /**\n * This event is emitted when the client requests for app setup to be performed. Addon should resolve the event with the metadata for the library entry. (See LibraryInfo)\n * @param data\n * @param event\n * @returns\n */\n setup: (\n data: {\n path: string;\n type: 'direct' | 'torrent' | 'magnet' | 'empty';\n name: string;\n usedRealDebrid: boolean;\n clearOldFilesBeforeUpdate?: boolean;\n multiPartFiles?: {\n name: string;\n downloadURL: string;\n }[];\n appID: number;\n storefront: string;\n manifest?: Record<string, unknown>;\n } & (\n | {\n for: 'game';\n }\n | {\n for: 'update';\n currentLibraryInfo: LibraryInfo;\n }\n ),\n event: EventResponse<SetupEventResponse>\n ) => void;\n\n /**\n * This event is emitted when the client requires for a search to be performed. Input is the search query.\n * @param query\n * @param event\n * @returns\n */\n 'library-search': (\n query: string,\n event: EventResponse<BasicLibraryInfo[]>\n ) => void;\n\n /**\n * This event is emitted when the client requests for a game details to be fetched. Addon should resolve the event with the game details. This is used to generate a store page for the game.\n * @param appID\n * @param event\n * @returns\n */\n 'game-details': (\n details: { appID: number; storefront: string },\n event: EventResponse<StoreData | undefined>\n ) => void;\n\n /**\n * This event is emitted when the client requests for the addon to exit. Use this to perform any cleanup tasks, ending with a `process.exit(0)`.\n * @returns\n */\n exit: () => void;\n\n /**\n * This event is emitted when the client requests for a download to be performed with the 'request' type. Addon should resolve the event with a SearchResult containing the actual download info.\n * @param appID\n * @param info\n * @param event\n * @returns\n */\n 'request-dl': (\n appID: number,\n info: SearchResult,\n event: EventResponse<SearchResult>\n ) => void;\n\n /**\n * This event is emitted when the client requests for a catalog to be fetched. Addon should resolve the event with the catalog.\n * @param event\n * @returns\n */\n catalog: (event: Omit<EventResponse<CatalogResponse>, 'askForInput'>) => void;\n\n /**\n * This event is emitted when the client requests for an addon to check for updates. Addon should resolve the event with the update information.\n * @param data\n * @param event\n * @returns\n */\n 'check-for-updates': (\n data: { appID: number; storefront: string; currentVersion: string },\n event: EventResponse<\n | {\n available: true;\n version: string;\n }\n | {\n available: false;\n }\n >\n ) => void;\n\n /**\n * This event is emitted when the client is going to launch an app. Addon should use this to perform any pre or post launch tasks.\n * @param data {LibraryInfo} The library information for the app to be launched.\n * @param launchType { 'pre' | 'post' } The type of launch task to perform.\n * @param event {EventResponse<void>} The event response from the server.\n */\n 'launch-app': (\n data: { libraryInfo: LibraryInfo; launchType: 'pre' | 'post' },\n event: EventResponse<void>\n ) => void;\n}\n\nexport interface StoreData {\n name: string;\n publishers: string[];\n developers: string[];\n appID: number;\n releaseDate: string;\n capsuleImage: string;\n coverImage: string;\n basicDescription: string;\n description: string;\n headerImage: string;\n latestVersion: string;\n}\nexport interface WebsocketMessageClient {\n event: OGIAddonClientSentEvent;\n id?: string;\n args: any;\n statusError?: string;\n}\nexport interface WebsocketMessageServer {\n event: OGIAddonServerSentEvent;\n id?: string;\n args: any;\n statusError?: string;\n}\n\n/**\n * The configuration for the addon. This is used to identify the addon and provide information about it.\n * Storefronts is an array of names of stores that the addon supports.\n */\nexport interface OGIAddonConfiguration {\n name: string;\n id: string;\n description: string;\n version: string;\n\n author: string;\n repository: string;\n storefronts: string[];\n}\n\n/**\n * The main class for the OGI Addon. This class is used to interact with the OGI Addon Server. The OGI Addon Server provides a `--addonSecret` to the addon so it can securely connect.\n * @example\n * ```typescript\n * const addon = new OGIAddon({\n * name: 'Test Addon',\n * id: 'test-addon',\n * description: 'A test addon',\n * version: '1.0.0',\n * author: 'OGI Developers',\n * repository: ''\n * });\n * ```\n *\n */\nexport default class OGIAddon {\n public eventEmitter = new events.EventEmitter();\n public addonWSListener: OGIAddonWSListener;\n public addonInfo: OGIAddonConfiguration;\n public config: Configuration = new Configuration({});\n private eventsAvailable: OGIAddonEvent[] = [];\n private registeredConnectEvent: boolean = false;\n private taskHandlers: Map<\n string,\n (\n task: Task,\n data: {\n manifest: Record<string, unknown>;\n downloadPath: string;\n name: string;\n libraryInfo: LibraryInfo;\n }\n ) => Promise<void> | void\n > = new Map();\n\n constructor(addonInfo: OGIAddonConfiguration) {\n this.addonInfo = addonInfo;\n this.addonWSListener = new OGIAddonWSListener(this, this.eventEmitter);\n }\n\n /**\n * Register an event listener for the addon. (See EventListenerTypes)\n * @param event {OGIAddonEvent}\n * @param listener {EventListenerTypes[OGIAddonEvent]}\n */\n public on<T extends OGIAddonEvent>(\n event: T,\n listener: EventListenerTypes[T]\n ) {\n this.eventEmitter.on(event, listener);\n this.eventsAvailable.push(event);\n // wait for the addon to be connected\n if (!this.registeredConnectEvent) {\n this.addonWSListener.eventEmitter.once('connect', () => {\n this.addonWSListener.send('flag', {\n flag: 'events-available',\n value: this.eventsAvailable,\n });\n });\n this.registeredConnectEvent = true;\n }\n }\n\n public emit<T extends OGIAddonEvent>(\n event: T,\n ...args: Parameters<EventListenerTypes[T]>\n ) {\n this.eventEmitter.emit(event, ...args);\n }\n\n /**\n * Notify the client using a notification. Provide the type of notification, the message, and an ID.\n * @param notification {Notification}\n */\n public notify(notification: Notification) {\n this.addonWSListener.send('notification', [notification]);\n }\n\n /**\n * Get the app details for a given appID and storefront.\n * @param appID {number}\n * @param storefront {string}\n * @returns {Promise<StoreData>}\n */\n public async getAppDetails(appID: number, storefront: string) {\n const id = this.addonWSListener.send('get-app-details', {\n appID,\n storefront,\n });\n return await this.addonWSListener.waitForResponseFromServer<\n StoreData | undefined\n >(id);\n }\n\n public async searchGame(query: string, storefront: string) {\n const id = this.addonWSListener.send('search-app-name', {\n query,\n storefront,\n });\n return await this.addonWSListener.waitForResponseFromServer<\n BasicLibraryInfo[]\n >(id);\n }\n\n /**\n * Notify the OGI Addon Server that you are performing a background task. This can be used to help users understand what is happening in the background.\n * @returns {Promise<Task>} A Task instance for managing the background task.\n */\n public async task(): Promise<Task> {\n const id = Math.random().toString(36).substring(7);\n const progress = 0;\n const logs: string[] = [];\n const task = new Task(this.addonWSListener, id, progress, logs);\n this.addonWSListener.send('task-update', {\n id,\n progress,\n logs,\n finished: false,\n failed: undefined,\n });\n return task;\n }\n\n /**\n * Register a task handler for a specific task name. The task name should match the taskName field in SearchResult or ActionOption.\n * @param taskName {string} The name of the task (should match taskName in SearchResult or ActionOption.setTaskName()).\n * @param handler {(task: Task, data: { manifest: Record<string, unknown>; downloadPath: string; name: string; libraryInfo: LibraryInfo }) => Promise<void> | void} The handler function.\n * @example\n * ```typescript\n * addon.onTask('clearCache', async (task) => {\n * task.log('Clearing cache...');\n * task.setProgress(50);\n * await clearCacheFiles();\n * task.setProgress(100);\n * task.complete();\n * });\n * ```\n */\n public onTask(\n taskName: string,\n handler: (\n task: Task,\n data: {\n manifest: Record<string, unknown>;\n downloadPath: string;\n name: string;\n libraryInfo: LibraryInfo;\n }\n ) => Promise<void> | void\n ): void {\n this.taskHandlers.set(taskName, handler);\n }\n\n /**\n * Check if a task handler is registered for the given task name.\n * @param taskName {string} The task name to check.\n * @returns {boolean} True if a handler is registered.\n */\n public hasTaskHandler(taskName: string): boolean {\n return this.taskHandlers.has(taskName);\n }\n\n /**\n * Get a task handler for the given task name.\n * @param taskName {string} The task name.\n * @returns The handler function or undefined if not found.\n */\n public getTaskHandler(taskName: string):\n | ((\n task: Task,\n data: {\n manifest: Record<string, unknown>;\n downloadPath: string;\n name: string;\n libraryInfo?: LibraryInfo;\n }\n ) => Promise<void> | void)\n | undefined {\n return this.taskHandlers.get(taskName);\n }\n\n /**\n * Extract a file using 7-Zip on Windows, unzip on Linux/Mac.\n * @param path {string}\n * @param outputPath {string}\n * @param type {'unrar' | 'unzip'}\n * @returns {Promise<void>}\n */\n public async extractFile(\n path: string,\n outputPath: string,\n type: 'unrar' | 'unzip'\n ) {\n return new Promise<void>((resolve, reject) => {\n // Ensure outputPath exists\n if (!fs.existsSync(outputPath)) {\n fs.mkdirSync(outputPath, { recursive: true });\n }\n\n if (type === 'unzip') {\n // Prefer 7-Zip on Windows, unzip on Linux/Mac\n if (process.platform === 'win32') {\n // 7-Zip path (default install location)\n const s7ZipPath = '\"C:\\\\Program Files\\\\7-Zip\\\\7z.exe\"';\n exec(\n `${s7ZipPath} x \"${path}\" -o\"${outputPath}\"`,\n (err: any, stdout: any, stderr: any) => {\n if (err) {\n console.error(err);\n console.log(stderr);\n reject(new Error('Failed to extract ZIP file'));\n return;\n }\n console.log(stdout);\n console.log(stderr);\n resolve();\n }\n );\n } else {\n // Use unzip on Linux/Mac\n const unzipProcess = spawn(\n 'unzip',\n [\n '-o', // overwrite files without prompting\n path,\n '-d', // specify output directory\n outputPath,\n ],\n {\n env: {\n ...process.env,\n UNZIP_DISABLE_ZIPBOMB_DETECTION: 'TRUE',\n },\n }\n );\n\n unzipProcess.stdout.on('data', (data: Buffer) => {\n console.log(`[unzip stdout]: ${data}`);\n });\n\n unzipProcess.stderr.on('data', (data: Buffer) => {\n console.error(`[unzip stderr]: ${data}`);\n });\n\n unzipProcess.on('close', (code: number) => {\n if (code !== 0) {\n console.error(`unzip process exited with code ${code}`);\n reject(new Error('Failed to extract ZIP file'));\n return;\n }\n resolve();\n });\n }\n } else if (type === 'unrar') {\n if (process.platform === 'win32') {\n // 7-Zip path (default install location)\n const s7ZipPath = '\"C:\\\\Program Files\\\\7-Zip\\\\7z.exe\"';\n exec(\n `${s7ZipPath} x \"${path}\" -o\"${outputPath}\"`,\n (err: any, stdout: any, stderr: any) => {\n if (err) {\n console.error(err);\n console.log(stderr);\n reject(new Error('Failed to extract RAR file'));\n return;\n }\n console.log(stdout);\n console.log(stderr);\n resolve();\n }\n );\n } else {\n // Use unrar on Linux/Mac\n const unrarProcess = spawn('unrar', ['x', '-y', path, outputPath]);\n\n unrarProcess.stdout.on('data', (data: Buffer) => {\n console.log(`[unrar stdout]: ${data}`);\n });\n\n unrarProcess.stderr.on('data', (data: Buffer) => {\n console.error(`[unrar stderr]: ${data}`);\n });\n\n unrarProcess.on('close', (code: number) => {\n if (code !== 0) {\n console.error(`unrar process exited with code ${code}`);\n reject(new Error('Failed to extract RAR file'));\n return;\n }\n resolve();\n });\n }\n } else {\n reject(new Error('Unknown extraction type'));\n }\n });\n }\n}\n\n/**\n * A unified task API for both server-initiated tasks (via onTask handlers)\n * and addon-initiated background tasks (via addon.task()).\n * Provides chainable methods for logging, progress updates, and completion.\n */\nexport class Task {\n // EventResponse-based mode (for onTask handlers)\n private event: EventResponse<void> | undefined;\n\n // WebSocket-based mode (for addon.task())\n private ws: OGIAddonWSListener | undefined;\n private readonly id: string | undefined;\n private progress: number = 0;\n private logs: string[] = [];\n private finished: boolean = false;\n private failed: string | undefined = undefined;\n\n /**\n * Construct a Task from an EventResponse (for onTask handlers).\n * @param event {EventResponse<void>} The event response to wrap.\n */\n constructor(event: EventResponse<void>);\n\n /**\n * Construct a Task from WebSocket listener (for addon.task()).\n * @param ws {OGIAddonWSListener} The WebSocket listener.\n * @param id {string} The task ID.\n * @param progress {number} Initial progress (0-100).\n * @param logs {string[]} Initial logs array.\n */\n constructor(\n ws: OGIAddonWSListener,\n id: string,\n progress: number,\n logs: string[]\n );\n\n constructor(\n eventOrWs: EventResponse<void> | OGIAddonWSListener,\n id?: string,\n progress?: number,\n logs?: string[]\n ) {\n if (eventOrWs instanceof EventResponse) {\n // EventResponse-based mode\n this.event = eventOrWs;\n this.event.defer();\n } else {\n // WebSocket-based mode\n this.ws = eventOrWs;\n this.id = id!;\n this.progress = progress ?? 0;\n this.logs = logs ?? [];\n }\n }\n\n /**\n * Log a message to the task. Returns this for chaining.\n * @param message {string} The message to log.\n */\n log(message: string): this {\n if (this.event) {\n this.event.log(message);\n } else {\n this.logs.push(message);\n this.update();\n }\n return this;\n }\n\n /**\n * Set the progress of the task (0-100). Returns this for chaining.\n * @param progress {number} The progress value (0-100).\n */\n setProgress(progress: number): this {\n if (this.event) {\n this.event.progress = progress;\n } else {\n this.progress = progress;\n this.update();\n }\n return this;\n }\n\n /**\n * Complete the task successfully.\n */\n complete(): void {\n if (this.event) {\n this.event.complete();\n } else {\n this.finished = true;\n this.update();\n }\n }\n\n /**\n * Fail the task with an error message.\n * @param message {string} The error message.\n */\n fail(message: string): void {\n if (this.event) {\n this.event.fail(message);\n } else {\n this.failed = message;\n this.update();\n }\n }\n\n /**\n * Ask the user for input using a ConfigurationBuilder screen.\n * Only available for EventResponse-based tasks (onTask handlers).\n * The return type is inferred from the ConfigurationBuilder's accumulated option types.\n * @param name {string} The name/title of the input prompt.\n * @param description {string} The description of what input is needed.\n * @param screen {ConfigurationBuilder<U>} The configuration builder for the input form.\n * @returns {Promise<U>} The user's input with types matching the configuration options.\n * @throws {Error} If called on a WebSocket-based task.\n */\n async askForInput<U extends Record<string, string | number | boolean>>(\n name: string,\n description: string,\n screen: ConfigurationBuilder<U>\n ): Promise<U> {\n if (!this.event) {\n throw new Error(\n 'askForInput() is only available for EventResponse-based tasks (onTask handlers)'\n );\n }\n return this.event.askForInput(name, description, screen);\n }\n\n /**\n * Update the task state (for WebSocket-based tasks only).\n * Called automatically when using log(), setProgress(), complete(), or fail().\n */\n private update(): void {\n if (this.ws && this.id !== undefined) {\n this.ws.send('task-update', {\n id: this.id,\n progress: this.progress,\n logs: this.logs,\n finished: this.finished,\n failed: this.failed,\n });\n }\n }\n}\n/**\n * A search tool wrapper over Fuse.js for the OGI Addon. This tool is used to search for items in the library.\n * @example\n * ```typescript\n * const searchTool = new SearchTool<LibraryInfo>([{ name: 'test', appID: 123 }, { name: 'test2', appID: 124 }], ['name']);\n * const results = searchTool.search('test', 10);\n * ```\n */\nexport class SearchTool<T> {\n private fuse: Fuse<T>;\n constructor(\n items: T[],\n keys: string[],\n options: Omit<IFuseOptions<T>, 'keys'> = {\n threshold: 0.3,\n includeScore: true,\n }\n ) {\n this.fuse = new Fuse(items, {\n keys,\n ...options,\n });\n }\n public search(query: string, limit: number = 10): T[] {\n return this.fuse\n .search(query)\n .slice(0, limit)\n .map((result) => result.item);\n }\n public addItems(items: T[]) {\n items.map((item) => this.fuse.add(item));\n }\n}\n/**\n * Library Info is the metadata for a library entry after setting up a game.\n */\nexport const ZodLibraryInfo = z.object({\n name: z.string(),\n version: z.string(),\n cwd: z.string(),\n appID: z.number(),\n launchExecutable: z.string(),\n launchArguments: z.string().optional(),\n capsuleImage: z.string(),\n storefront: z.string(),\n addonsource: z.string(),\n coverImage: z.string(),\n titleImage: z.string().optional(),\n /**\n * UMU Proton integration configuration (Linux only)\n */\n umu: z\n .object({\n umuId: z\n .string()\n .regex(/^(steam|umu):\\d+$/, 'Must be in format steam:{number} or umu:{number}'),\n dllOverrides: z.array(z.string()).optional(),\n protonVersion: z.string().optional(),\n store: z.string().optional(),\n winePrefixPath: z.string().optional(),\n steamShortcutId: z.number().optional(),\n })\n .optional(),\n /**\n * Legacy mode flag for games using old Steam/flatpak wine system\n */\n legacyMode: z.boolean().optional(),\n /**\n * Redistributables to install (for backward compatibility)\n */\n redistributables: z\n .array(\n z.object({\n name: z.string(),\n path: z.string(),\n })\n )\n .optional(),\n});\nexport type LibraryInfo = z.infer<typeof ZodLibraryInfo>;\ninterface Notification {\n type: 'warning' | 'error' | 'info' | 'success';\n message: string;\n id: string;\n}\nclass OGIAddonWSListener {\n private socket: WebSocket;\n public eventEmitter: events.EventEmitter;\n public addon: OGIAddon;\n\n constructor(ogiAddon: OGIAddon, eventEmitter: events.EventEmitter) {\n if (\n process.argv[process.argv.length - 1].split('=')[0] !== '--addonSecret'\n ) {\n throw new Error(\n 'No secret provided. This usually happens because the addon was not started by the OGI Addon Server.'\n );\n }\n this.addon = ogiAddon;\n this.eventEmitter = eventEmitter;\n this.socket = new ws('ws://localhost:' + defaultPort);\n this.socket.on('open', () => {\n console.log('Connected to OGI Addon Server');\n console.log('OGI Addon Server Version:', VERSION);\n\n // Authenticate with OGI Addon Server\n this.send('authenticate', {\n ...this.addon.addonInfo,\n secret: process.argv[process.argv.length - 1].split('=')[1],\n ogiVersion: VERSION,\n });\n\n // send a configuration request\n let configBuilder = new ConfigurationBuilder();\n this.eventEmitter.emit('configure', configBuilder);\n this.send('configure', configBuilder.build(false));\n this.addon.config = new Configuration(configBuilder.build(true));\n\n // wait for the config-update to be received then send connect\n const configListener = (event: ws.MessageEvent) => {\n if (event === undefined) return;\n // event can be a Buffer, string, ArrayBuffer, or Buffer[]\n let data: string;\n if (typeof event === 'string') {\n data = event;\n } else if (event instanceof Buffer) {\n data = event.toString();\n } else if (event && typeof (event as any).data === 'string') {\n data = (event as any).data;\n } else if (event && (event as any).data instanceof Buffer) {\n data = (event as any).data.toString();\n } else {\n // fallback for other types\n data = event.toString();\n }\n const message: WebsocketMessageServer = JSON.parse(data);\n if (message.event === 'config-update') {\n console.log('Config update received');\n this.socket.off('message', configListener);\n this.eventEmitter.emit(\n 'connect',\n new EventResponse<void>((screen, name, description) => {\n return this.userInputAsked(\n screen,\n name,\n description,\n this.socket\n );\n })\n );\n }\n };\n this.socket.on('message', configListener);\n });\n\n this.socket.on('error', (error) => {\n if (error.message.includes('Failed to connect')) {\n throw new Error(\n 'OGI Addon Server is not running/is unreachable. Please start the server and try again.'\n );\n }\n console.error('An error occurred:', error);\n });\n\n this.socket.on('close', (code, reason) => {\n if (code === 1008) {\n console.error('Authentication failed:', reason);\n return;\n }\n this.eventEmitter.emit('disconnect', reason);\n console.log('Disconnected from OGI Addon Server');\n console.error(reason.toString());\n this.eventEmitter.emit('exit');\n this.socket.close();\n });\n\n this.registerMessageReceiver();\n }\n\n private async userInputAsked<\n U extends Record<string, string | number | boolean>,\n >(\n configBuilt: ConfigurationBuilder<U>,\n name: string,\n description: string,\n socket: WebSocket\n ): Promise<U> {\n const config = configBuilt.build(false);\n const id = Math.random().toString(36).substring(7);\n if (!socket) {\n throw new Error('Socket is not connected');\n }\n socket.send(\n JSON.stringify({\n event: 'input-asked',\n args: {\n config,\n name,\n description,\n },\n id: id,\n })\n );\n return await this.waitForResponseFromServer<U>(id);\n }\n\n /**\n * Registers the message receiver for the socket. This is used to receive messages from the server and handle them.\n */\n private registerMessageReceiver() {\n this.socket.on('message', async (data: string) => {\n const message: WebsocketMessageServer = JSON.parse(data);\n switch (message.event) {\n case 'config-update':\n const result = this.addon.config.updateConfig(message.args);\n if (!result[0]) {\n this.respondToMessage(\n message.id!!,\n {\n success: false,\n error: result[1],\n },\n undefined\n );\n } else {\n this.respondToMessage(message.id!!, { success: true }, undefined);\n }\n break;\n case 'search':\n await this.handleEventWithResponse<SearchResult[]>(message, (event) =>\n this.eventEmitter.emit('search', message.args, event)\n );\n break;\n case 'setup': {\n let setupEvent = new EventResponse<SetupEventResponse>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n this.eventEmitter.emit('setup', message.args, setupEvent);\n const interval = setInterval(() => {\n if (setupEvent.resolved) {\n clearInterval(interval);\n return;\n }\n this.send('defer-update', {\n logs: setupEvent.logs,\n deferID: message.args.deferID,\n progress: setupEvent.progress,\n failed: setupEvent.failed,\n } as ClientSentEventTypes['defer-update']);\n }, 100);\n const setupResult = await this.waitForEventToRespond(setupEvent);\n this.respondToMessage(message.id!!, setupResult.data, setupEvent);\n break;\n }\n case 'library-search':\n await this.handleEventWithResponse<BasicLibraryInfo[]>(\n message,\n (event) =>\n this.eventEmitter.emit('library-search', message.args, event)\n );\n break;\n case 'game-details':\n await this.handleEventWithResponse<StoreData | undefined>(\n message,\n (event) =>\n this.eventEmitter.emit('game-details', message.args, event),\n {\n requireListener: 'game-details',\n noListenerError: 'No event listener for game-details',\n }\n );\n break;\n case 'check-for-updates':\n await this.handleEventWithResponse<\n { available: true; version: string } | { available: false }\n >(message, (event) =>\n this.eventEmitter.emit('check-for-updates', message.args, event)\n );\n break;\n case 'request-dl':\n let requestDLEvent = new EventResponse<SearchResult>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n if (this.eventEmitter.listenerCount('request-dl') === 0) {\n this.respondToMessage(\n message.id!!,\n {\n error: 'No event listener for request-dl',\n },\n requestDLEvent\n );\n break;\n }\n this.eventEmitter.emit(\n 'request-dl',\n message.args.appID,\n message.args.info,\n requestDLEvent\n );\n const requestDLResult =\n await this.waitForEventToRespond(requestDLEvent);\n if (requestDLEvent.failed) {\n this.respondToMessage(message.id!!, undefined, requestDLEvent);\n break;\n }\n if (\n requestDLEvent.data === undefined ||\n requestDLEvent.data?.downloadType === 'request'\n ) {\n throw new Error(\n 'Request DL event did not return a valid result. Please ensure that the event does not resolve with another `request` download type.'\n );\n }\n this.respondToMessage(\n message.id!!,\n requestDLResult.data,\n requestDLEvent\n );\n break;\n case 'catalog':\n await this.handleEventWithResponseNoInput<CatalogResponse>(\n message,\n (event) => this.eventEmitter.emit('catalog', event)\n );\n break;\n case 'task-run': {\n let taskRunEvent = new EventResponse<void>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n\n // Check for taskName: first from args directly (from SearchResult), then from manifest.__taskName (for ActionOption)\n const taskName =\n message.args.taskName && typeof message.args.taskName === 'string'\n ? message.args.taskName\n : message.args.manifest &&\n typeof message.args.manifest === 'object'\n ? (message.args.manifest as Record<string, unknown>).__taskName\n : undefined;\n\n if (\n taskName &&\n typeof taskName === 'string' &&\n this.addon.hasTaskHandler(taskName)\n ) {\n // Use the registered task handler\n const handler = this.addon.getTaskHandler(taskName)!;\n const task = new Task(taskRunEvent);\n try {\n const interval = setInterval(() => {\n if (taskRunEvent.resolved) {\n clearInterval(interval);\n return;\n }\n this.send('defer-update', {\n logs: taskRunEvent.logs,\n deferID: message.args.deferID,\n progress: taskRunEvent.progress,\n failed: taskRunEvent.failed,\n } as ClientSentEventTypes['defer-update']);\n }, 100);\n const result = handler(task, {\n manifest: message.args.manifest || {},\n downloadPath: message.args.downloadPath || '',\n name: message.args.name || '',\n libraryInfo: message.args.libraryInfo,\n });\n // If handler returns a promise, wait for it\n if (result instanceof Promise) {\n await result;\n }\n\n clearInterval(interval);\n } catch (error) {\n taskRunEvent.fail(\n error instanceof Error ? error.message : String(error)\n );\n }\n } else {\n // No handler found - fail the task\n taskRunEvent.fail(\n taskName\n ? `No task handler registered for task name: ${taskName}`\n : 'No task name provided'\n );\n }\n\n const taskRunResult = await this.waitForEventToRespond(taskRunEvent);\n this.respondToMessage(message.id!!, taskRunResult.data, taskRunEvent);\n break;\n }\n case 'launch-app':\n await this.handleEventWithResponse<void>(message, (event) =>\n this.eventEmitter.emit('launch-app', message.args, event)\n );\n break;\n }\n });\n }\n\n private waitForEventToRespond<T>(\n event: EventResponse<T>\n ): Promise<EventResponse<T>> {\n // check the handlers to see if there even is any\n return new Promise((resolve, reject) => {\n const dataGet = setInterval(() => {\n if (event.resolved) {\n resolve(event);\n clearTimeout(timeout);\n }\n }, 5);\n\n const timeout = setTimeout(() => {\n if (event.deffered) {\n clearInterval(dataGet);\n const interval = setInterval(() => {\n if (event.resolved) {\n clearInterval(interval);\n resolve(event);\n }\n }, 100);\n } else {\n reject('Event did not respond in time');\n }\n }, 5000);\n });\n }\n\n /**\n * Common flow for events that use EventResponse with userInputAsked: create event, emit via callback, wait, respond.\n * If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.\n */\n private async handleEventWithResponse<T>(\n message: WebsocketMessageServer,\n emit: (event: EventResponse<T>) => void,\n options?: { requireListener: string; noListenerError: string }\n ): Promise<void> {\n const event = new EventResponse<T>((screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n if (\n options &&\n this.eventEmitter.listenerCount(options.requireListener) === 0\n ) {\n this.respondToMessage(\n message.id!!,\n { error: options.noListenerError },\n event\n );\n return;\n }\n emit(event);\n const result = await this.waitForEventToRespond(event);\n this.respondToMessage(message.id!!, result.data, event);\n }\n\n /**\n * Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).\n */\n private async handleEventWithResponseNoInput<T>(\n message: WebsocketMessageServer,\n emit: (event: EventResponse<T>) => void\n ): Promise<void> {\n const event = new EventResponse<T>();\n emit(event);\n const result = await this.waitForEventToRespond(event);\n this.respondToMessage(message.id!!, result.data, event);\n }\n\n public respondToMessage(\n messageID: string,\n response: any,\n originalEvent: EventResponse<any> | undefined\n ) {\n this.socket.send(\n JSON.stringify({\n event: 'response',\n id: messageID,\n args: response,\n statusError: originalEvent ? originalEvent.failed : undefined,\n })\n );\n console.log('dispatched response to ' + messageID);\n }\n\n public waitForResponseFromServer<T>(messageID: string): Promise<T> {\n return new Promise((resolve) => {\n const waiter = (data: string) => {\n const message: WebsocketMessageClient = JSON.parse(data);\n if (message.event !== 'response') {\n this.socket.once('message', waiter);\n return;\n }\n console.log('received response from ' + messageID);\n\n if (message.id === messageID) {\n resolve(message.args);\n } else {\n this.socket.once('message', waiter);\n }\n };\n this.socket.once('message', waiter);\n });\n }\n\n public send(\n event: OGIAddonClientSentEvent,\n args: ClientSentEventTypes[OGIAddonClientSentEvent]\n ): string {\n // generate a random id\n const id = Math.random().toString(36).substring(7);\n this.socket.send(\n JSON.stringify({\n event,\n args,\n id,\n })\n );\n return id;\n }\n\n public close() {\n this.socket.close();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"main.mjs","names":["pjson.version","z"],"sources":["../package.json","../src/main.ts"],"sourcesContent":["","import ws, { WebSocket } from 'ws';\nimport events from 'node:events';\nimport { ConfigurationBuilder } from './config/ConfigurationBuilder';\nimport type { ConfigurationFile } from './config/ConfigurationBuilder';\nimport { Configuration } from './config/Configuration';\nimport EventResponse from './EventResponse';\nimport type { SearchResult } from './SearchEngine';\nimport Fuse, { IFuseOptions } from 'fuse.js';\n\n/**\n * Exposed events that the programmer can use to listen to and emit events.\n */\nexport type OGIAddonEvent =\n | 'connect'\n | 'disconnect'\n | 'configure'\n | 'authenticate'\n | 'search'\n | 'setup'\n | 'library-search'\n | 'game-details'\n | 'exit'\n | 'check-for-updates'\n | 'request-dl'\n | 'catalog'\n | 'launch-app';\n\n/**\n * The events that the client can send to the server and are handled by the server.\n */\nexport type OGIAddonClientSentEvent =\n | 'response'\n | 'authenticate'\n | 'configure'\n | 'defer-update'\n | 'notification'\n | 'input-asked'\n | 'get-app-details'\n | 'search-app-name'\n | 'flag'\n | 'task-update';\n\n/**\n * The events that the server sends to the client\n * This is the events that the server can send to the client and are handled by the client.\n */\nexport type OGIAddonServerSentEvent =\n | 'authenticate'\n | 'configure'\n | 'config-update'\n | 'launch-app'\n | 'search'\n | 'setup'\n | 'response'\n | 'library-search'\n | 'check-for-updates'\n | 'task-run'\n | 'game-details'\n | 'request-dl'\n | 'catalog';\nexport { ConfigurationBuilder, Configuration, EventResponse };\nexport { extraction };\nexport type { SearchResult };\nconst defaultPort = 7654;\nimport pjson from '../package.json';\nimport { z } from 'zod';\nimport { extraction } from './extraction';\nexport const VERSION = pjson.version;\n\nexport interface ClientSentEventTypes {\n response: any;\n authenticate: {\n name: string;\n id: string;\n description: string;\n version: string;\n author: string;\n };\n configure: ConfigurationFile;\n 'defer-update': {\n logs: string[];\n progress: number;\n };\n notification: Notification;\n 'input-asked': ConfigurationBuilder<\n Record<string, string | number | boolean>\n >;\n 'task-update': {\n id: string;\n progress: number;\n logs: string[];\n finished: boolean;\n failed: string | undefined;\n };\n 'get-app-details': {\n appID: number;\n storefront: string;\n };\n 'search-app-name': {\n query: string;\n storefront: string;\n };\n flag: {\n flag: string;\n value: string | string[];\n };\n}\n\nexport type BasicLibraryInfo = {\n name: string;\n capsuleImage: string;\n appID: number;\n storefront: string;\n};\n\nexport interface CatalogSection {\n name: string;\n description: string;\n listings: BasicLibraryInfo[];\n}\n\nexport interface CatalogCarouselItem {\n name: string;\n description: string;\n carouselImage: string;\n fullBannerImage?: string;\n appID?: number;\n storefront?: string;\n capsuleImage?: string;\n}\n\nexport interface CatalogWithCarousel {\n sections: Record<string, CatalogSection>;\n carousel?: Record<string, CatalogCarouselItem> | CatalogCarouselItem[];\n}\n\nexport type CatalogResponse =\n | Record<string, CatalogSection>\n | CatalogWithCarousel;\n\n/**\n * UMU ID format: 'steam:${number}' or 'umu:${string | number}'\n * - steam:${number} → maps to umu-${number} for Steam games\n * - umu:${string | number} → maps to umu-${string | number} for non-Steam games\n */\nexport type UmuId = `steam:${number}` | `umu:${string | number}`;\n\nexport type SetupEventResponse = Omit<\n LibraryInfo,\n | 'capsuleImage'\n | 'coverImage'\n | 'name'\n | 'appID'\n | 'storefront'\n | 'addonsource'\n | 'titleImage'\n> & {\n redistributables?: {\n name: string;\n path: string;\n }[];\n /**\n * UMU Proton integration configuration\n */\n umu?: {\n /**\n * UMU ID for the game. Format: 'steam:${number}' or 'umu:${string | number}'\n * - steam:${number} → maps to umu-${number} for Steam games\n * - umu:${string | number} → maps to umu-${string | number} for non-Steam games\n */\n umuId: UmuId;\n /**\n * Optional DLL overrides. Can be WINEDLLOVERRIDES-style (e.g. \"dinput8=n,b\") or bare DLL names.\n * Bare names get \"=n,b\" inferred; entries that already include \"=...\" are used as-is.\n */\n dllOverrides?: string[];\n /**\n * Optional PROTONPATH override to use for UMU launches.\n * Omit this unless you absolutely need a specific Proton build/path.\n */\n protonVersion?: string;\n /**\n * Optional store identifier for protonfixes (e.g., 'gog', 'egs', 'none')\n */\n store?: string;\n /**\n * Cached Steam shortcut app ID after adding UMU game to Steam (avoids re-adding on each launch)\n */\n steamShortcutId?: number;\n };\n};\n\nexport interface EventListenerTypes {\n /**\n * This event is emitted when the addon connects to the OGI Addon Server. Addon does not need to resolve anything.\n * @param event\n * @returns\n */\n connect: (event: EventResponse<void>) => void;\n\n /**\n * This event is emitted when the client requests for the addon to disconnect. Addon does not need to resolve this event, but we recommend `process.exit(0)` so the addon can exit gracefully instead of by force by the addon server.\n * @param reason\n * @returns\n */\n disconnect: (reason: string) => void;\n /**\n * This event is emitted when the client requests for the addon to configure itself. Addon should resolve the event with the internal configuration. (See ConfigurationBuilder)\n * @param config\n * @returns\n */\n configure: (config: ConfigurationBuilder) => ConfigurationBuilder;\n /**\n * This event is called when the client provides a response to any event. This should be treated as middleware.\n * @param response\n * @returns\n */\n response: (response: any) => void;\n\n /**\n * This event is called when the client requests for the addon to authenticate itself. You don't need to provide any info.\n * @param config\n * @returns\n */\n authenticate: (config: any) => void;\n /**\n * This event is emitted when the client requests for a torrent/direct download search to be performed. Addon is given the gameID (could be a steam appID or custom store appID), along with the storefront type. Addon should resolve the event with the search results. (See SearchResult)\n * @param query\n * @param event\n * @returns\n */\n search: (\n query: {\n storefront: string;\n appID: number;\n } & (\n | {\n for: 'game' | 'task' | 'all';\n }\n | {\n for: 'update';\n libraryInfo: LibraryInfo;\n }\n ),\n event: EventResponse<SearchResult[]>\n ) => void;\n /**\n * This event is emitted when the client requests for app setup to be performed. Addon should resolve the event with the metadata for the library entry. (See LibraryInfo)\n * @param data\n * @param event\n * @returns\n */\n setup: (\n data: {\n path: string;\n type: 'direct' | 'torrent' | 'magnet' | 'empty';\n name: string;\n usedRealDebrid: boolean;\n clearOldFilesBeforeUpdate?: boolean;\n multiPartFiles?: {\n name: string;\n downloadURL: string;\n }[];\n appID: number;\n storefront: string;\n manifest?: Record<string, unknown>;\n } & (\n | {\n for: 'game';\n }\n | {\n for: 'update';\n currentLibraryInfo: LibraryInfo;\n }\n ),\n event: EventResponse<SetupEventResponse>\n ) => void;\n\n /**\n * This event is emitted when the client requires for a search to be performed. Input is the search query.\n * @param query\n * @param event\n * @returns\n */\n 'library-search': (\n query: string,\n event: EventResponse<BasicLibraryInfo[]>\n ) => void;\n\n /**\n * This event is emitted when the client requests for a game details to be fetched. Addon should resolve the event with the game details. This is used to generate a store page for the game.\n * @param appID\n * @param event\n * @returns\n */\n 'game-details': (\n details: { appID: number; storefront: string },\n event: EventResponse<StoreData | undefined>\n ) => void;\n\n /**\n * This event is emitted when the client requests for the addon to exit. Use this to perform any cleanup tasks, ending with a `process.exit(0)`.\n * @returns\n */\n exit: () => void;\n\n /**\n * This event is emitted when the client requests for a download to be performed with the 'request' type. Addon should resolve the event with a SearchResult containing the actual download info.\n * @param appID\n * @param info\n * @param event\n * @returns\n */\n 'request-dl': (\n appID: number,\n info: SearchResult,\n event: EventResponse<SearchResult>\n ) => void;\n\n /**\n * This event is emitted when the client requests for a catalog to be fetched. Addon should resolve the event with the catalog.\n * @param event\n * @returns\n */\n catalog: (event: Omit<EventResponse<CatalogResponse>, 'askForInput'>) => void;\n\n /**\n * This event is emitted when the client requests for an addon to check for updates. Addon should resolve the event with the update information.\n * @param data\n * @param event\n * @returns\n */\n 'check-for-updates': (\n data: { appID: number; storefront: string; currentVersion: string },\n event: EventResponse<\n | {\n available: true;\n version: string;\n }\n | {\n available: false;\n }\n >\n ) => void;\n\n /**\n * This event is emitted when the client is going to launch an app. Addon should use this to perform any pre or post launch tasks.\n * @param data {LibraryInfo} The library information for the app to be launched.\n * @param launchType { 'pre' | 'post' } The type of launch task to perform.\n * @param event {EventResponse<void>} The event response from the server.\n */\n 'launch-app': (\n data: { libraryInfo: LibraryInfo; launchType: 'pre' | 'post' },\n event: EventResponse<void>\n ) => void;\n}\n\nexport interface StoreData {\n name: string;\n publishers: string[];\n developers: string[];\n appID: number;\n releaseDate: string;\n capsuleImage: string;\n coverImage: string;\n basicDescription: string;\n description: string;\n headerImage: string;\n latestVersion: string;\n}\nexport interface WebsocketMessageClient {\n event: OGIAddonClientSentEvent;\n id?: string;\n args: any;\n statusError?: string;\n}\nexport interface WebsocketMessageServer {\n event: OGIAddonServerSentEvent;\n id?: string;\n args: any;\n statusError?: string;\n}\n\n/**\n * The configuration for the addon. This is used to identify the addon and provide information about it.\n * Storefronts is an array of names of stores that the addon supports.\n */\nexport interface OGIAddonConfiguration {\n name: string;\n id: string;\n description: string;\n version: string;\n\n author: string;\n repository: string;\n storefronts: string[];\n}\n\n/**\n * The main class for the OGI Addon. This class is used to interact with the OGI Addon Server. The OGI Addon Server provides a `--addonSecret` to the addon so it can securely connect.\n * @example\n * ```typescript\n * const addon = new OGIAddon({\n * name: 'Test Addon',\n * id: 'test-addon',\n * description: 'A test addon',\n * version: '1.0.0',\n * author: 'OGI Developers',\n * repository: ''\n * });\n * ```\n *\n */\nexport default class OGIAddon {\n public eventEmitter = new events.EventEmitter();\n public addonWSListener: OGIAddonWSListener;\n public addonInfo: OGIAddonConfiguration;\n public config: Configuration = new Configuration({});\n private eventsAvailable: OGIAddonEvent[] = [];\n private registeredConnectEvent: boolean = false;\n private taskHandlers: Map<\n string,\n (\n task: Task,\n data: {\n manifest: Record<string, unknown>;\n downloadPath: string;\n name: string;\n libraryInfo: LibraryInfo;\n }\n ) => Promise<void> | void\n > = new Map();\n\n constructor(addonInfo: OGIAddonConfiguration) {\n this.addonInfo = addonInfo;\n this.addonWSListener = new OGIAddonWSListener(this, this.eventEmitter);\n }\n\n /**\n * Register an event listener for the addon. (See EventListenerTypes)\n * @param event {OGIAddonEvent}\n * @param listener {EventListenerTypes[OGIAddonEvent]}\n */\n public on<T extends OGIAddonEvent>(\n event: T,\n listener: EventListenerTypes[T]\n ) {\n this.eventEmitter.on(event, listener);\n this.eventsAvailable.push(event);\n // wait for the addon to be connected\n if (!this.registeredConnectEvent) {\n this.addonWSListener.eventEmitter.once('connect', () => {\n this.addonWSListener.send('flag', {\n flag: 'events-available',\n value: this.eventsAvailable,\n });\n });\n this.registeredConnectEvent = true;\n }\n }\n\n public emit<T extends OGIAddonEvent>(\n event: T,\n ...args: Parameters<EventListenerTypes[T]>\n ) {\n this.eventEmitter.emit(event, ...args);\n }\n\n /**\n * Notify the client using a notification. Provide the type of notification, the message, and an ID.\n * @param notification {Notification}\n */\n public notify(notification: Notification) {\n this.addonWSListener.send('notification', [notification]);\n }\n\n /**\n * Get the app details for a given appID and storefront.\n * @param appID {number}\n * @param storefront {string}\n * @returns {Promise<StoreData>}\n */\n public async getAppDetails(appID: number, storefront: string) {\n const id = this.addonWSListener.send('get-app-details', {\n appID,\n storefront,\n });\n return await this.addonWSListener.waitForResponseFromServer<\n StoreData | undefined\n >(id);\n }\n\n public async searchGame(query: string, storefront: string) {\n const id = this.addonWSListener.send('search-app-name', {\n query,\n storefront,\n });\n return await this.addonWSListener.waitForResponseFromServer<\n BasicLibraryInfo[]\n >(id);\n }\n\n /**\n * Notify the OGI Addon Server that you are performing a background task. This can be used to help users understand what is happening in the background.\n * @returns {Promise<Task>} A Task instance for managing the background task.\n */\n public async task(): Promise<Task> {\n const id = Math.random().toString(36).substring(7);\n const progress = 0;\n const logs: string[] = [];\n const task = new Task(this.addonWSListener, id, progress, logs);\n this.addonWSListener.send('task-update', {\n id,\n progress,\n logs,\n finished: false,\n failed: undefined,\n });\n return task;\n }\n\n /**\n * Register a task handler for a specific task name. The task name should match the taskName field in SearchResult or ActionOption.\n * @param taskName {string} The name of the task (should match taskName in SearchResult or ActionOption.setTaskName()).\n * @param handler {(task: Task, data: { manifest: Record<string, unknown>; downloadPath: string; name: string; libraryInfo: LibraryInfo }) => Promise<void> | void} The handler function.\n * @example\n * ```typescript\n * addon.onTask('clearCache', async (task) => {\n * task.log('Clearing cache...');\n * task.setProgress(50);\n * await clearCacheFiles();\n * task.setProgress(100);\n * task.complete();\n * });\n * ```\n */\n public onTask(\n taskName: string,\n handler: (\n task: Task,\n data: {\n manifest: Record<string, unknown>;\n downloadPath: string;\n name: string;\n libraryInfo: LibraryInfo;\n }\n ) => Promise<void> | void\n ): void {\n this.taskHandlers.set(taskName, handler);\n }\n\n /**\n * Check if a task handler is registered for the given task name.\n * @param taskName {string} The task name to check.\n * @returns {boolean} True if a handler is registered.\n */\n public hasTaskHandler(taskName: string): boolean {\n return this.taskHandlers.has(taskName);\n }\n\n /**\n * Get a task handler for the given task name.\n * @param taskName {string} The task name.\n * @returns The handler function or undefined if not found.\n */\n public getTaskHandler(taskName: string):\n | ((\n task: Task,\n data: {\n manifest: Record<string, unknown>;\n downloadPath: string;\n name: string;\n libraryInfo?: LibraryInfo;\n }\n ) => Promise<void> | void)\n | undefined {\n return this.taskHandlers.get(taskName);\n }\n\n /**\n * Extract a file using 7-Zip on Windows, unzip on Linux/Mac.\n * @param path {string}\n * @param outputPath {string}\n * @returns {Promise<void>}\n */\n public async extractFile(path: string, outputPath: string) {\n return await extraction(path, outputPath);\n }\n}\n\n/**\n * A unified task API for both server-initiated tasks (via onTask handlers)\n * and addon-initiated background tasks (via addon.task()).\n * Provides chainable methods for logging, progress updates, and completion.\n */\nexport class Task {\n // EventResponse-based mode (for onTask handlers)\n private event: EventResponse<void> | undefined;\n\n // WebSocket-based mode (for addon.task())\n private ws: OGIAddonWSListener | undefined;\n private readonly id: string | undefined;\n private progress: number = 0;\n private logs: string[] = [];\n private finished: boolean = false;\n private failed: string | undefined = undefined;\n\n /**\n * Construct a Task from an EventResponse (for onTask handlers).\n * @param event {EventResponse<void>} The event response to wrap.\n */\n constructor(event: EventResponse<void>);\n\n /**\n * Construct a Task from WebSocket listener (for addon.task()).\n * @param ws {OGIAddonWSListener} The WebSocket listener.\n * @param id {string} The task ID.\n * @param progress {number} Initial progress (0-100).\n * @param logs {string[]} Initial logs array.\n */\n constructor(\n ws: OGIAddonWSListener,\n id: string,\n progress: number,\n logs: string[]\n );\n\n constructor(\n eventOrWs: EventResponse<void> | OGIAddonWSListener,\n id?: string,\n progress?: number,\n logs?: string[]\n ) {\n if (eventOrWs instanceof EventResponse) {\n // EventResponse-based mode\n this.event = eventOrWs;\n this.event.defer();\n } else {\n // WebSocket-based mode\n this.ws = eventOrWs;\n this.id = id!;\n this.progress = progress ?? 0;\n this.logs = logs ?? [];\n }\n }\n\n /**\n * Log a message to the task. Returns this for chaining.\n * @param message {string} The message to log.\n */\n log(message: string): this {\n if (this.event) {\n this.event.log(message);\n } else {\n this.logs.push(message);\n this.update();\n }\n return this;\n }\n\n /**\n * Set the progress of the task (0-100). Returns this for chaining.\n * @param progress {number} The progress value (0-100).\n */\n setProgress(progress: number): this {\n if (this.event) {\n this.event.progress = progress;\n } else {\n this.progress = progress;\n this.update();\n }\n return this;\n }\n\n /**\n * Complete the task successfully.\n */\n complete(): void {\n if (this.event) {\n this.event.complete();\n } else {\n this.finished = true;\n this.update();\n }\n }\n\n /**\n * Fail the task with an error message.\n * @param message {string} The error message.\n */\n fail(message: string): void {\n if (this.event) {\n this.event.fail(message);\n } else {\n this.failed = message;\n this.update();\n }\n }\n\n /**\n * Ask the user for input using a ConfigurationBuilder screen.\n * Only available for EventResponse-based tasks (onTask handlers).\n * The return type is inferred from the ConfigurationBuilder's accumulated option types.\n * @param name {string} The name/title of the input prompt.\n * @param description {string} The description of what input is needed.\n * @param screen {ConfigurationBuilder<U>} The configuration builder for the input form.\n * @returns {Promise<U>} The user's input with types matching the configuration options.\n * @throws {Error} If called on a WebSocket-based task.\n */\n async askForInput<U extends Record<string, string | number | boolean>>(\n name: string,\n description: string,\n screen: ConfigurationBuilder<U>\n ): Promise<U> {\n if (!this.event) {\n throw new Error(\n 'askForInput() is only available for EventResponse-based tasks (onTask handlers)'\n );\n }\n return this.event.askForInput(name, description, screen);\n }\n\n /**\n * Update the task state (for WebSocket-based tasks only).\n * Called automatically when using log(), setProgress(), complete(), or fail().\n */\n private update(): void {\n if (this.ws && this.id !== undefined) {\n this.ws.send('task-update', {\n id: this.id,\n progress: this.progress,\n logs: this.logs,\n finished: this.finished,\n failed: this.failed,\n });\n }\n }\n}\n/**\n * A search tool wrapper over Fuse.js for the OGI Addon. This tool is used to search for items in the library.\n * @example\n * ```typescript\n * const searchTool = new SearchTool<LibraryInfo>([{ name: 'test', appID: 123 }, { name: 'test2', appID: 124 }], ['name']);\n * const results = searchTool.search('test', 10);\n * ```\n */\nexport class SearchTool<T> {\n private fuse: Fuse<T>;\n constructor(\n items: T[],\n keys: string[],\n options: Omit<IFuseOptions<T>, 'keys'> = {\n threshold: 0.3,\n includeScore: true,\n }\n ) {\n this.fuse = new Fuse(items, {\n keys,\n ...options,\n });\n }\n public search(query: string, limit: number = 10): T[] {\n return this.fuse\n .search(query)\n .slice(0, limit)\n .map((result) => result.item);\n }\n public addItems(items: T[]) {\n items.map((item) => this.fuse.add(item));\n }\n}\n/**\n * Library Info is the metadata for a library entry after setting up a game.\n */\nexport const ZodLibraryInfo = z.object({\n name: z.string(),\n version: z.string(),\n cwd: z.string(),\n appID: z.number(),\n launchExecutable: z.string(),\n launchArguments: z.string().optional(),\n launchEnv: z.record(z.string(), z.string()).optional(),\n capsuleImage: z.string(),\n storefront: z.string(),\n addonsource: z.string(),\n coverImage: z.string(),\n titleImage: z.string().optional(),\n /**\n * UMU Proton integration configuration (Linux only)\n */\n umu: z\n .object({\n umuId: z\n .string()\n .regex(\n /^(steam|umu):\\S+$/,\n 'Must be in format steam:{number} or umu:{string | number}'\n ),\n dllOverrides: z.array(z.string()).optional(),\n protonVersion: z.string().optional(),\n store: z.string().optional(),\n winePrefixPath: z.string().optional(),\n steamShortcutId: z.number().optional(),\n })\n .optional(),\n /**\n * Redistributables to install (for backward compatibility)\n */\n redistributables: z\n .array(\n z.object({\n name: z.string(),\n path: z.string(),\n })\n )\n .optional(),\n});\nexport type LibraryInfo = z.infer<typeof ZodLibraryInfo>;\ninterface Notification {\n type: 'warning' | 'error' | 'info' | 'success';\n message: string;\n id: string;\n}\nclass OGIAddonWSListener {\n private socket: WebSocket;\n public eventEmitter: events.EventEmitter;\n public addon: OGIAddon;\n\n constructor(ogiAddon: OGIAddon, eventEmitter: events.EventEmitter) {\n if (\n process.argv[process.argv.length - 1].split('=')[0] !== '--addonSecret'\n ) {\n throw new Error(\n 'No secret provided. This usually happens because the addon was not started by the OGI Addon Server.'\n );\n }\n this.addon = ogiAddon;\n this.eventEmitter = eventEmitter;\n this.socket = new ws('ws://localhost:' + defaultPort);\n this.socket.on('open', () => {\n console.log('Connected to OGI Addon Server');\n console.log('OGI Addon Server Version:', VERSION);\n\n // Authenticate with OGI Addon Server\n this.send('authenticate', {\n ...this.addon.addonInfo,\n secret: process.argv[process.argv.length - 1].split('=')[1],\n ogiVersion: VERSION,\n });\n\n // send a configuration request\n let configBuilder = new ConfigurationBuilder();\n this.eventEmitter.emit('configure', configBuilder);\n this.send('configure', configBuilder.build(false));\n this.addon.config = new Configuration(configBuilder.build(true));\n\n // wait for the config-update to be received then send connect\n const configListener = (event: ws.MessageEvent) => {\n if (event === undefined) return;\n // event can be a Buffer, string, ArrayBuffer, or Buffer[]\n let data: string;\n if (typeof event === 'string') {\n data = event;\n } else if (event instanceof Buffer) {\n data = event.toString();\n } else if (event && typeof (event as any).data === 'string') {\n data = (event as any).data;\n } else if (event && (event as any).data instanceof Buffer) {\n data = (event as any).data.toString();\n } else {\n // fallback for other types\n data = event.toString();\n }\n const message: WebsocketMessageServer = JSON.parse(data);\n if (message.event === 'config-update') {\n console.log('Config update received');\n this.socket.off('message', configListener);\n this.eventEmitter.emit(\n 'connect',\n new EventResponse<void>((screen, name, description) => {\n return this.userInputAsked(\n screen,\n name,\n description,\n this.socket\n );\n })\n );\n }\n };\n this.socket.on('message', configListener);\n });\n\n this.socket.on('error', (error) => {\n if (error.message.includes('Failed to connect')) {\n throw new Error(\n 'OGI Addon Server is not running/is unreachable. Please start the server and try again.'\n );\n }\n console.error('An error occurred:', error);\n });\n\n this.socket.on('close', (code, reason) => {\n if (code === 1008) {\n console.error('Authentication failed:', reason);\n return;\n }\n this.eventEmitter.emit('disconnect', reason);\n console.log('Disconnected from OGI Addon Server');\n console.error(reason.toString());\n this.eventEmitter.emit('exit');\n this.socket.close();\n });\n\n this.registerMessageReceiver();\n }\n\n private async userInputAsked<\n U extends Record<string, string | number | boolean>,\n >(\n configBuilt: ConfigurationBuilder<U>,\n name: string,\n description: string,\n socket: WebSocket\n ): Promise<U> {\n const config = configBuilt.build(false);\n const id = Math.random().toString(36).substring(7);\n if (!socket) {\n throw new Error('Socket is not connected');\n }\n socket.send(\n JSON.stringify({\n event: 'input-asked',\n args: {\n config,\n name,\n description,\n },\n id: id,\n })\n );\n return await this.waitForResponseFromServer<U>(id);\n }\n\n /**\n * Registers the message receiver for the socket. This is used to receive messages from the server and handle them.\n */\n private registerMessageReceiver() {\n this.socket.on('message', async (data: string) => {\n const message: WebsocketMessageServer = JSON.parse(data);\n switch (message.event) {\n case 'config-update':\n const result = this.addon.config.updateConfig(message.args);\n if (!result[0]) {\n this.respondToMessage(\n message.id!!,\n {\n success: false,\n error: result[1],\n },\n undefined\n );\n } else {\n this.respondToMessage(message.id!!, { success: true }, undefined);\n }\n break;\n case 'search':\n await this.handleEventWithResponse<SearchResult[]>(message, (event) =>\n this.eventEmitter.emit('search', message.args, event)\n );\n break;\n case 'setup': {\n let setupEvent = new EventResponse<SetupEventResponse>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n this.eventEmitter.emit('setup', message.args, setupEvent);\n const interval = setInterval(() => {\n if (setupEvent.resolved) {\n clearInterval(interval);\n return;\n }\n this.send('defer-update', {\n logs: setupEvent.logs,\n deferID: message.args.deferID,\n progress: setupEvent.progress,\n failed: setupEvent.failed,\n } as ClientSentEventTypes['defer-update']);\n }, 100);\n const setupResult = await this.waitForEventToRespond(setupEvent);\n this.respondToMessage(message.id!!, setupResult.data, setupEvent);\n break;\n }\n case 'library-search':\n await this.handleEventWithResponse<BasicLibraryInfo[]>(\n message,\n (event) =>\n this.eventEmitter.emit('library-search', message.args, event)\n );\n break;\n case 'game-details':\n await this.handleEventWithResponse<StoreData | undefined>(\n message,\n (event) =>\n this.eventEmitter.emit('game-details', message.args, event),\n {\n requireListener: 'game-details',\n noListenerError: 'No event listener for game-details',\n }\n );\n break;\n case 'check-for-updates':\n await this.handleEventWithResponse<\n { available: true; version: string } | { available: false }\n >(message, (event) =>\n this.eventEmitter.emit('check-for-updates', message.args, event)\n );\n break;\n case 'request-dl':\n let requestDLEvent = new EventResponse<SearchResult>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n if (this.eventEmitter.listenerCount('request-dl') === 0) {\n this.respondToMessage(\n message.id!!,\n {\n error: 'No event listener for request-dl',\n },\n requestDLEvent\n );\n break;\n }\n this.eventEmitter.emit(\n 'request-dl',\n message.args.appID,\n message.args.info,\n requestDLEvent\n );\n const requestDLResult =\n await this.waitForEventToRespond(requestDLEvent);\n if (requestDLEvent.failed) {\n this.respondToMessage(message.id!!, undefined, requestDLEvent);\n break;\n }\n if (\n requestDLEvent.data === undefined ||\n requestDLEvent.data?.downloadType === 'request'\n ) {\n throw new Error(\n 'Request DL event did not return a valid result. Please ensure that the event does not resolve with another `request` download type.'\n );\n }\n this.respondToMessage(\n message.id!!,\n requestDLResult.data,\n requestDLEvent\n );\n break;\n case 'catalog':\n await this.handleEventWithResponseNoInput<CatalogResponse>(\n message,\n (event) => this.eventEmitter.emit('catalog', event)\n );\n break;\n case 'task-run': {\n let taskRunEvent = new EventResponse<void>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n\n // Check for taskName: first from args directly (from SearchResult), then from manifest.__taskName (for ActionOption)\n const taskName =\n message.args.taskName && typeof message.args.taskName === 'string'\n ? message.args.taskName\n : message.args.manifest &&\n typeof message.args.manifest === 'object'\n ? (message.args.manifest as Record<string, unknown>).__taskName\n : undefined;\n\n if (\n taskName &&\n typeof taskName === 'string' &&\n this.addon.hasTaskHandler(taskName)\n ) {\n // Use the registered task handler\n const handler = this.addon.getTaskHandler(taskName)!;\n const task = new Task(taskRunEvent);\n try {\n const interval = setInterval(() => {\n if (taskRunEvent.resolved) {\n clearInterval(interval);\n return;\n }\n this.send('defer-update', {\n logs: taskRunEvent.logs,\n deferID: message.args.deferID,\n progress: taskRunEvent.progress,\n failed: taskRunEvent.failed,\n } as ClientSentEventTypes['defer-update']);\n }, 100);\n const result = handler(task, {\n manifest: message.args.manifest || {},\n downloadPath: message.args.downloadPath || '',\n name: message.args.name || '',\n libraryInfo: message.args.libraryInfo,\n });\n // If handler returns a promise, wait for it\n if (result instanceof Promise) {\n await result;\n }\n\n clearInterval(interval);\n } catch (error) {\n taskRunEvent.fail(\n error instanceof Error ? error.message : String(error)\n );\n }\n } else {\n // No handler found - fail the task\n taskRunEvent.fail(\n taskName\n ? `No task handler registered for task name: ${taskName}`\n : 'No task name provided'\n );\n }\n\n const taskRunResult = await this.waitForEventToRespond(taskRunEvent);\n this.respondToMessage(message.id!!, taskRunResult.data, taskRunEvent);\n break;\n }\n case 'launch-app':\n await this.handleEventWithResponse<void>(message, (event) =>\n this.eventEmitter.emit('launch-app', message.args, event)\n );\n break;\n }\n });\n }\n\n private waitForEventToRespond<T>(\n event: EventResponse<T>\n ): Promise<EventResponse<T>> {\n // check the handlers to see if there even is any\n return new Promise((resolve, reject) => {\n const dataGet = setInterval(() => {\n if (event.resolved) {\n resolve(event);\n clearTimeout(timeout);\n }\n }, 5);\n\n const timeout = setTimeout(() => {\n if (event.deffered) {\n clearInterval(dataGet);\n const interval = setInterval(() => {\n if (event.resolved) {\n clearInterval(interval);\n resolve(event);\n }\n }, 100);\n } else {\n reject('Event did not respond in time');\n }\n }, 5000);\n });\n }\n\n /**\n * Common flow for events that use EventResponse with userInputAsked: create event, emit via callback, wait, respond.\n * If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.\n */\n private async handleEventWithResponse<T>(\n message: WebsocketMessageServer,\n emit: (event: EventResponse<T>) => void,\n options?: { requireListener: string; noListenerError: string }\n ): Promise<void> {\n const event = new EventResponse<T>((screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n if (\n options &&\n this.eventEmitter.listenerCount(options.requireListener) === 0\n ) {\n this.respondToMessage(\n message.id!!,\n { error: options.noListenerError },\n event\n );\n return;\n }\n emit(event);\n const result = await this.waitForEventToRespond(event);\n this.respondToMessage(message.id!!, result.data, event);\n }\n\n /**\n * Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).\n */\n private async handleEventWithResponseNoInput<T>(\n message: WebsocketMessageServer,\n emit: (event: EventResponse<T>) => void\n ): Promise<void> {\n const event = new EventResponse<T>();\n emit(event);\n const result = await this.waitForEventToRespond(event);\n this.respondToMessage(message.id!!, result.data, event);\n }\n\n public respondToMessage(\n messageID: string,\n response: any,\n originalEvent: EventResponse<any> | undefined\n ) {\n this.socket.send(\n JSON.stringify({\n event: 'response',\n id: messageID,\n args: response,\n statusError: originalEvent ? originalEvent.failed : undefined,\n })\n );\n console.log('dispatched response to ' + messageID);\n }\n\n public waitForResponseFromServer<T>(messageID: string): Promise<T> {\n return new Promise((resolve) => {\n const waiter = (data: string) => {\n const message: WebsocketMessageClient = JSON.parse(data);\n if (message.event !== 'response') {\n this.socket.once('message', waiter);\n return;\n }\n console.log('received response from ' + messageID);\n\n if (message.id === messageID) {\n resolve(message.args);\n } else {\n this.socket.once('message', waiter);\n }\n };\n this.socket.once('message', waiter);\n });\n }\n\n public send(\n event: OGIAddonClientSentEvent,\n args: ClientSentEventTypes[OGIAddonClientSentEvent]\n ): string {\n // generate a random id\n const id = Math.random().toString(36).substring(7);\n this.socket.send(\n JSON.stringify({\n event,\n args,\n id,\n })\n );\n return id;\n }\n\n public close() {\n this.socket.close();\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "./build/main.mjs",
4
4
  "type": "module",
5
5
  "main": "./build/main.cjs",
6
- "version": "2.4.0",
6
+ "version": "3.1.0",
7
7
  "exports": {
8
8
  ".": {
9
9
  "import": {
@@ -47,6 +47,7 @@
47
47
  "build": "tsdown --config tsdown.config.js",
48
48
  "typecheck": "tsc --noEmit",
49
49
  "release": "bun run build && npm publish",
50
+ "format": "prettier --write \"src/**/*.{ts,svelte,json}\"",
50
51
  "release-beta": "bun run build && npm publish --tag future"
51
52
  },
52
53
  "devDependencies": {
@@ -38,11 +38,11 @@ export function isActionOption<N extends string = string>(
38
38
  * A builder for creating configuration screens. The generic type T accumulates
39
39
  * the types of all options added to the builder, enabling type-safe access to
40
40
  * the configuration values.
41
- *
41
+ *
42
42
  * @template T - The accumulated type of all configuration options
43
43
  */
44
44
  export class ConfigurationBuilder<
45
- T extends Record<string, string | number | boolean> = {}
45
+ T extends Record<string, string | number | boolean> = {},
46
46
  > {
47
47
  private options: ConfigurationOption<string>[] = [];
48
48
 
@@ -89,7 +89,7 @@ export class ConfigurationBuilder<
89
89
  }
90
90
 
91
91
  /**
92
- * Add an action option to the configuration builder and return the builder for chaining.
92
+ * Add an action option to the configuration builder and return the builder for chaining.
93
93
  * Action options contribute a boolean to the return type (true if clicked, false if not).
94
94
  * You must provide a name, display name, and description for the option.
95
95
  * @param option { (option: ActionOption) => ActionOption<K> }
@@ -175,7 +175,9 @@ export class ConfigurationOption<N extends string = string> {
175
175
  }
176
176
  }
177
177
 
178
- export class StringOption<N extends string = string> extends ConfigurationOption<N> {
178
+ export class StringOption<
179
+ N extends string = string,
180
+ > extends ConfigurationOption<N> {
179
181
  public allowedValues: string[] = [];
180
182
  public minTextLength: number = 0;
181
183
  public maxTextLength: number = Number.MAX_SAFE_INTEGER;
@@ -266,7 +268,9 @@ export class StringOption<N extends string = string> extends ConfigurationOption
266
268
  }
267
269
  }
268
270
 
269
- export class NumberOption<N extends string = string> extends ConfigurationOption<N> {
271
+ export class NumberOption<
272
+ N extends string = string,
273
+ > extends ConfigurationOption<N> {
270
274
  public min: number = 0;
271
275
  public max: number = Number.MAX_SAFE_INTEGER;
272
276
  public defaultValue: number = 0;
@@ -332,7 +336,9 @@ export class NumberOption<N extends string = string> extends ConfigurationOption
332
336
  }
333
337
  }
334
338
 
335
- export class BooleanOption<N extends string = string> extends ConfigurationOption<N> {
339
+ export class BooleanOption<
340
+ N extends string = string,
341
+ > extends ConfigurationOption<N> {
336
342
  public type: ConfigurationOptionType = 'boolean';
337
343
  public defaultValue: boolean = false;
338
344
 
@@ -362,7 +368,9 @@ export class BooleanOption<N extends string = string> extends ConfigurationOptio
362
368
  }
363
369
  }
364
370
 
365
- export class ActionOption<N extends string = string> extends ConfigurationOption<N> {
371
+ export class ActionOption<
372
+ N extends string = string,
373
+ > extends ConfigurationOption<N> {
366
374
  public type: ConfigurationOptionType = 'action';
367
375
  public manifest: Record<string, unknown> = {};
368
376
  public buttonText: string = 'Run';
@@ -0,0 +1,87 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ const s7ZipPath = 'C:\\Program Files\\7-Zip\\7z.exe';
4
+
5
+ function waitForChildProcess(
6
+ childProcess: ReturnType<typeof spawn>,
7
+ errorMessage: string
8
+ ): Promise<void> {
9
+ return new Promise<void>((resolve, reject) => {
10
+ childProcess.once('error', reject);
11
+ childProcess.once('close', (code) => {
12
+ if (code !== 0) {
13
+ reject(new Error(errorMessage));
14
+ return;
15
+ }
16
+
17
+ resolve();
18
+ });
19
+ });
20
+ }
21
+
22
+ async function detectUnrarType(): Promise<
23
+ 'unrar-free' | 'unrar-nonfree' | 'unknown'
24
+ > {
25
+ const childProcess = spawn('unrar');
26
+
27
+ return await new Promise((resolve, reject) => {
28
+ let output = '';
29
+
30
+ const collectOutput = (data: Buffer) => {
31
+ output += data.toString();
32
+ };
33
+
34
+ childProcess.stdout.on('data', collectOutput);
35
+ childProcess.stderr.on('data', collectOutput);
36
+ childProcess.once('error', reject);
37
+ childProcess.once('close', () => {
38
+ if (output.includes('unrar-free')) {
39
+ resolve('unrar-free');
40
+ return;
41
+ }
42
+
43
+ if (output.includes('unrar-nonfree')) {
44
+ resolve('unrar-nonfree');
45
+ return;
46
+ }
47
+
48
+ resolve('unknown');
49
+ });
50
+ });
51
+ }
52
+
53
+ export async function extraction(filePath: string, outputDir: string) {
54
+ const lowerCaseFilePath = filePath.toLowerCase();
55
+
56
+ if (process.platform === 'win32') {
57
+ // expect 7zip to be installed, and use 7zip to unrar
58
+ const childProcess = spawn(s7ZipPath, ['x', filePath, '-o', outputDir]);
59
+ return await waitForChildProcess(childProcess, 'Failed to extract file');
60
+ } else if (process.platform === 'linux' || process.platform === 'darwin') {
61
+ if (lowerCaseFilePath.endsWith('.zip')) {
62
+ // expect unzip to be installed, and use unzip to unzip
63
+ const childProcess = spawn('unzip', ['-o', filePath, '-d', outputDir]);
64
+ return await waitForChildProcess(childProcess, 'Failed to unzip file');
65
+ } else if (lowerCaseFilePath.endsWith('.rar')) {
66
+ // check if unrar-nonfree is installed or unrar is installed
67
+ const unrarType = await detectUnrarType();
68
+
69
+ // now use the according unrar version to unrar
70
+ if (unrarType === 'unrar-free') {
71
+ // use unrar-free to unrar
72
+ const childProcess = spawn('unrar', ['-f', '-x', filePath, outputDir]);
73
+ return await waitForChildProcess(childProcess, 'Failed to unrar file');
74
+ } else if (unrarType === 'unrar-nonfree') {
75
+ // use unrar-nonfree to unrar
76
+ const childProcess = spawn('unrar', ['-o', filePath, '-d', outputDir]);
77
+ return await waitForChildProcess(childProcess, 'Failed to unrar file');
78
+ } else {
79
+ throw new Error('Unknown unrar type');
80
+ }
81
+ }
82
+
83
+ throw new Error(`Unsupported archive type: ${filePath}`);
84
+ }
85
+
86
+ throw new Error(`Unsupported platform: ${process.platform}`);
87
+ }
package/src/main.ts CHANGED
@@ -59,12 +59,12 @@ export type OGIAddonServerSentEvent =
59
59
  | 'request-dl'
60
60
  | 'catalog';
61
61
  export { ConfigurationBuilder, Configuration, EventResponse };
62
+ export { extraction };
62
63
  export type { SearchResult };
63
64
  const defaultPort = 7654;
64
65
  import pjson from '../package.json';
65
- import { exec, spawn } from 'node:child_process';
66
- import fs from 'node:fs';
67
66
  import { z } from 'zod';
67
+ import { extraction } from './extraction';
68
68
  export const VERSION = pjson.version;
69
69
 
70
70
  export interface ClientSentEventTypes {
@@ -139,11 +139,11 @@ export type CatalogResponse =
139
139
  | CatalogWithCarousel;
140
140
 
141
141
  /**
142
- * UMU ID format: 'steam:${number}' or 'umu:${number}'
142
+ * UMU ID format: 'steam:${number}' or 'umu:${string | number}'
143
143
  * - steam:${number} → maps to umu-${number} for Steam games
144
- * - umu:${number} → maps to umu-${number} for non-Steam games
144
+ * - umu:${string | number} → maps to umu-${string | number} for non-Steam games
145
145
  */
146
- export type UmuId = `steam:${number}` | `umu:${number}`;
146
+ export type UmuId = `steam:${number}` | `umu:${string | number}`;
147
147
 
148
148
  export type SetupEventResponse = Omit<
149
149
  LibraryInfo,
@@ -164,19 +164,19 @@ export type SetupEventResponse = Omit<
164
164
  */
165
165
  umu?: {
166
166
  /**
167
- * UMU ID for the game. Format: 'steam:${number}' or 'umu:${number}'
167
+ * UMU ID for the game. Format: 'steam:${number}' or 'umu:${string | number}'
168
168
  * - steam:${number} → maps to umu-${number} for Steam games
169
- * - umu:${number} → maps to umu-${number} for non-Steam games
169
+ * - umu:${string | number} → maps to umu-${string | number} for non-Steam games
170
170
  */
171
171
  umuId: UmuId;
172
172
  /**
173
- * Optional DLL overrides. These are relative to the game's cwd.
174
- * System automatically prepends cwd and sets WINEDLLOVERRIDES="dll=n,b"
173
+ * Optional DLL overrides. Can be WINEDLLOVERRIDES-style (e.g. "dinput8=n,b") or bare DLL names.
174
+ * Bare names get "=n,b" inferred; entries that already include "=..." are used as-is.
175
175
  */
176
176
  dllOverrides?: string[];
177
177
  /**
178
- * Optional Proton version to use (e.g., 'GE-Proton9-5', 'GE-Proton')
179
- * If not specified, uses latest UMU-Proton
178
+ * Optional PROTONPATH override to use for UMU launches.
179
+ * Omit this unless you absolutely need a specific Proton build/path.
180
180
  */
181
181
  protonVersion?: string;
182
182
  /**
@@ -581,117 +581,10 @@ export default class OGIAddon {
581
581
  * Extract a file using 7-Zip on Windows, unzip on Linux/Mac.
582
582
  * @param path {string}
583
583
  * @param outputPath {string}
584
- * @param type {'unrar' | 'unzip'}
585
584
  * @returns {Promise<void>}
586
585
  */
587
- public async extractFile(
588
- path: string,
589
- outputPath: string,
590
- type: 'unrar' | 'unzip'
591
- ) {
592
- return new Promise<void>((resolve, reject) => {
593
- // Ensure outputPath exists
594
- if (!fs.existsSync(outputPath)) {
595
- fs.mkdirSync(outputPath, { recursive: true });
596
- }
597
-
598
- if (type === 'unzip') {
599
- // Prefer 7-Zip on Windows, unzip on Linux/Mac
600
- if (process.platform === 'win32') {
601
- // 7-Zip path (default install location)
602
- const s7ZipPath = '"C:\\Program Files\\7-Zip\\7z.exe"';
603
- exec(
604
- `${s7ZipPath} x "${path}" -o"${outputPath}"`,
605
- (err: any, stdout: any, stderr: any) => {
606
- if (err) {
607
- console.error(err);
608
- console.log(stderr);
609
- reject(new Error('Failed to extract ZIP file'));
610
- return;
611
- }
612
- console.log(stdout);
613
- console.log(stderr);
614
- resolve();
615
- }
616
- );
617
- } else {
618
- // Use unzip on Linux/Mac
619
- const unzipProcess = spawn(
620
- 'unzip',
621
- [
622
- '-o', // overwrite files without prompting
623
- path,
624
- '-d', // specify output directory
625
- outputPath,
626
- ],
627
- {
628
- env: {
629
- ...process.env,
630
- UNZIP_DISABLE_ZIPBOMB_DETECTION: 'TRUE',
631
- },
632
- }
633
- );
634
-
635
- unzipProcess.stdout.on('data', (data: Buffer) => {
636
- console.log(`[unzip stdout]: ${data}`);
637
- });
638
-
639
- unzipProcess.stderr.on('data', (data: Buffer) => {
640
- console.error(`[unzip stderr]: ${data}`);
641
- });
642
-
643
- unzipProcess.on('close', (code: number) => {
644
- if (code !== 0) {
645
- console.error(`unzip process exited with code ${code}`);
646
- reject(new Error('Failed to extract ZIP file'));
647
- return;
648
- }
649
- resolve();
650
- });
651
- }
652
- } else if (type === 'unrar') {
653
- if (process.platform === 'win32') {
654
- // 7-Zip path (default install location)
655
- const s7ZipPath = '"C:\\Program Files\\7-Zip\\7z.exe"';
656
- exec(
657
- `${s7ZipPath} x "${path}" -o"${outputPath}"`,
658
- (err: any, stdout: any, stderr: any) => {
659
- if (err) {
660
- console.error(err);
661
- console.log(stderr);
662
- reject(new Error('Failed to extract RAR file'));
663
- return;
664
- }
665
- console.log(stdout);
666
- console.log(stderr);
667
- resolve();
668
- }
669
- );
670
- } else {
671
- // Use unrar on Linux/Mac
672
- const unrarProcess = spawn('unrar', ['x', '-y', path, outputPath]);
673
-
674
- unrarProcess.stdout.on('data', (data: Buffer) => {
675
- console.log(`[unrar stdout]: ${data}`);
676
- });
677
-
678
- unrarProcess.stderr.on('data', (data: Buffer) => {
679
- console.error(`[unrar stderr]: ${data}`);
680
- });
681
-
682
- unrarProcess.on('close', (code: number) => {
683
- if (code !== 0) {
684
- console.error(`unrar process exited with code ${code}`);
685
- reject(new Error('Failed to extract RAR file'));
686
- return;
687
- }
688
- resolve();
689
- });
690
- }
691
- } else {
692
- reject(new Error('Unknown extraction type'));
693
- }
694
- });
586
+ public async extractFile(path: string, outputPath: string) {
587
+ return await extraction(path, outputPath);
695
588
  }
696
589
  }
697
590
 
@@ -886,6 +779,7 @@ export const ZodLibraryInfo = z.object({
886
779
  appID: z.number(),
887
780
  launchExecutable: z.string(),
888
781
  launchArguments: z.string().optional(),
782
+ launchEnv: z.record(z.string(), z.string()).optional(),
889
783
  capsuleImage: z.string(),
890
784
  storefront: z.string(),
891
785
  addonsource: z.string(),
@@ -898,7 +792,10 @@ export const ZodLibraryInfo = z.object({
898
792
  .object({
899
793
  umuId: z
900
794
  .string()
901
- .regex(/^(steam|umu):\d+$/, 'Must be in format steam:{number} or umu:{number}'),
795
+ .regex(
796
+ /^(steam|umu):\S+$/,
797
+ 'Must be in format steam:{number} or umu:{string | number}'
798
+ ),
902
799
  dllOverrides: z.array(z.string()).optional(),
903
800
  protonVersion: z.string().optional(),
904
801
  store: z.string().optional(),
@@ -906,10 +803,6 @@ export const ZodLibraryInfo = z.object({
906
803
  steamShortcutId: z.number().optional(),
907
804
  })
908
805
  .optional(),
909
- /**
910
- * Legacy mode flag for games using old Steam/flatpak wine system
911
- */
912
- legacyMode: z.boolean().optional(),
913
806
  /**
914
807
  * Redistributables to install (for backward compatibility)
915
808
  */