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.
- package/build/config/ConfigurationBuilder.cjs +2 -2
- package/build/config/ConfigurationBuilder.cjs.map +1 -1
- package/build/config/ConfigurationBuilder.mjs +2 -2
- package/build/config/ConfigurationBuilder.mjs.map +1 -1
- package/build/extraction.cjs +80 -0
- package/build/extraction.cjs.map +1 -0
- package/build/extraction.d.cts +5 -0
- package/build/extraction.d.mts +5 -0
- package/build/extraction.mjs +78 -0
- package/build/extraction.mjs.map +1 -0
- package/build/main.cjs +7 -81
- package/build/main.cjs.map +1 -1
- package/build/main.d.cts +15 -18
- package/build/main.d.mts +15 -18
- package/build/main.mjs +7 -81
- package/build/main.mjs.map +1 -1
- package/package.json +2 -1
- package/src/config/ConfigurationBuilder.ts +15 -7
- package/src/extraction.ts +87 -0
- package/src/main.ts +18 -125
package/build/main.mjs.map
CHANGED
|
@@ -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": "
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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.
|
|
174
|
-
*
|
|
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
|
|
179
|
-
*
|
|
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
|
|
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(
|
|
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
|
*/
|