ogi-addon 2.3.0 → 2.3.2

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.
@@ -2,6 +2,12 @@
2
2
  type BaseRequiredFields = {
3
3
  name: string;
4
4
  manifest?: Record<string, any>;
5
+ /**
6
+ * Update-only hint for OGI's setup stage.
7
+ * When false, OGI keeps existing files in place instead of moving them to `old_files`
8
+ * before running update setup. Defaults to true/undefined behavior.
9
+ */
10
+ clearOldFilesBeforeUpdate?: boolean;
5
11
  };
6
12
  type SearchResult = BaseRequiredFields & ({
7
13
  downloadType: 'torrent' | 'magnet';
@@ -2,6 +2,12 @@
2
2
  type BaseRequiredFields = {
3
3
  name: string;
4
4
  manifest?: Record<string, any>;
5
+ /**
6
+ * Update-only hint for OGI's setup stage.
7
+ * When false, OGI keeps existing files in place instead of moving them to `old_files`
8
+ * before running update setup. Defaults to true/undefined behavior.
9
+ */
10
+ clearOldFilesBeforeUpdate?: boolean;
5
11
  };
6
12
  type SearchResult = BaseRequiredFields & ({
7
13
  downloadType: 'torrent' | 'magnet';
package/build/main.cjs CHANGED
@@ -15,7 +15,7 @@ let node_fs = require("node:fs");
15
15
  node_fs = require_chunk.__toESM(node_fs);
16
16
 
17
17
  //#region package.json
18
- var version = "2.3.0";
18
+ var version = "2.3.2";
19
19
 
20
20
  //#endregion
21
21
  //#region src/main.ts
@@ -441,6 +441,9 @@ var OGIAddonWSListener = class {
441
441
  }));
442
442
  return await this.waitForResponseFromServer(id);
443
443
  }
444
+ /**
445
+ * Registers the message receiver for the socket. This is used to receive messages from the server and handle them.
446
+ */
444
447
  registerMessageReceiver() {
445
448
  this.socket.on("message", async (data) => {
446
449
  const message = JSON.parse(data);
@@ -454,10 +457,7 @@ var OGIAddonWSListener = class {
454
457
  else this.respondToMessage(message.id, { success: true }, void 0);
455
458
  break;
456
459
  case "search":
457
- let searchResultEvent = new require_EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
458
- this.eventEmitter.emit("search", message.args, searchResultEvent);
459
- const searchResult = await this.waitForEventToRespond(searchResultEvent);
460
- this.respondToMessage(message.id, searchResult.data, searchResultEvent);
460
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("search", message.args, event));
461
461
  break;
462
462
  case "setup": {
463
463
  let setupEvent = new require_EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
@@ -479,26 +479,16 @@ var OGIAddonWSListener = class {
479
479
  break;
480
480
  }
481
481
  case "library-search":
482
- let librarySearchEvent = new require_EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
483
- this.eventEmitter.emit("library-search", message.args, librarySearchEvent);
484
- const librarySearchResult = await this.waitForEventToRespond(librarySearchEvent);
485
- this.respondToMessage(message.id, librarySearchResult.data, librarySearchEvent);
482
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("library-search", message.args, event));
486
483
  break;
487
484
  case "game-details":
488
- let gameDetailsEvent = new require_EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
489
- if (this.eventEmitter.listenerCount("game-details") === 0) {
490
- this.respondToMessage(message.id, { error: "No event listener for game-details" }, gameDetailsEvent);
491
- break;
492
- }
493
- this.eventEmitter.emit("game-details", message.args, gameDetailsEvent);
494
- const gameDetailsResult = await this.waitForEventToRespond(gameDetailsEvent);
495
- this.respondToMessage(message.id, gameDetailsResult.data, gameDetailsEvent);
485
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("game-details", message.args, event), {
486
+ requireListener: "game-details",
487
+ noListenerError: "No event listener for game-details"
488
+ });
496
489
  break;
497
490
  case "check-for-updates":
498
- let checkForUpdatesEvent = new require_EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
499
- this.eventEmitter.emit("check-for-updates", message.args, checkForUpdatesEvent);
500
- const checkForUpdatesResult = await this.waitForEventToRespond(checkForUpdatesEvent);
501
- this.respondToMessage(message.id, checkForUpdatesResult.data, checkForUpdatesEvent);
491
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("check-for-updates", message.args, event));
502
492
  break;
503
493
  case "request-dl":
504
494
  let requestDLEvent = new require_EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
@@ -516,10 +506,7 @@ var OGIAddonWSListener = class {
516
506
  this.respondToMessage(message.id, requestDLResult.data, requestDLEvent);
517
507
  break;
518
508
  case "catalog":
519
- let catalogEvent = new require_EventResponse();
520
- this.eventEmitter.emit("catalog", catalogEvent);
521
- const catalogResult = await this.waitForEventToRespond(catalogEvent);
522
- this.respondToMessage(message.id, catalogResult.data, catalogEvent);
509
+ await this.handleEventWithResponseNoInput(message, (event) => this.eventEmitter.emit("catalog", event));
523
510
  break;
524
511
  case "task-run": {
525
512
  let taskRunEvent = new require_EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
@@ -556,6 +543,9 @@ var OGIAddonWSListener = class {
556
543
  this.respondToMessage(message.id, taskRunResult.data, taskRunEvent);
557
544
  break;
558
545
  }
546
+ case "launch-app":
547
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("launch-app", message.args, event));
548
+ break;
559
549
  }
560
550
  });
561
551
  }
@@ -580,6 +570,29 @@ var OGIAddonWSListener = class {
580
570
  }, 5e3);
581
571
  });
582
572
  }
573
+ /**
574
+ * Common flow for events that use EventResponse with userInputAsked: create event, emit via callback, wait, respond.
575
+ * If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.
576
+ */
577
+ async handleEventWithResponse(message, emit, options) {
578
+ const event = new require_EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
579
+ if (options && this.eventEmitter.listenerCount(options.requireListener) === 0) {
580
+ this.respondToMessage(message.id, { error: options.noListenerError }, event);
581
+ return;
582
+ }
583
+ emit(event);
584
+ const result = await this.waitForEventToRespond(event);
585
+ this.respondToMessage(message.id, result.data, event);
586
+ }
587
+ /**
588
+ * Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).
589
+ */
590
+ async handleEventWithResponseNoInput(message, emit) {
591
+ const event = new require_EventResponse();
592
+ emit(event);
593
+ const result = await this.waitForEventToRespond(event);
594
+ this.respondToMessage(message.id, result.data, event);
595
+ }
583
596
  respondToMessage(messageID, response, originalEvent) {
584
597
  this.socket.send(JSON.stringify({
585
598
  event: "response",
@@ -1 +1 @@
1
- {"version":3,"file":"main.cjs","names":["pjson.version","events","Configuration","fs","EventResponse","Fuse","z","ConfigurationBuilder"],"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\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\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\nexport type OGIAddonServerSentEvent =\n | 'authenticate'\n | 'configure'\n | 'config-update'\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 = Record<string, CatalogSection> | CatalogWithCarousel;\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\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 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: (\n event: Omit<\n EventResponse<CatalogResponse>,\n 'askForInput'\n >\n ) => 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\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});\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 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 let searchResultEvent = new EventResponse<SearchResult[]>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n this.eventEmitter.emit('search', message.args, searchResultEvent);\n const searchResult =\n await this.waitForEventToRespond(searchResultEvent);\n this.respondToMessage(\n message.id!!,\n searchResult.data,\n searchResultEvent\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 let librarySearchEvent = new EventResponse<BasicLibraryInfo[]>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n this.eventEmitter.emit(\n 'library-search',\n message.args,\n librarySearchEvent\n );\n const librarySearchResult =\n await this.waitForEventToRespond(librarySearchEvent);\n this.respondToMessage(\n message.id!!,\n librarySearchResult.data,\n librarySearchEvent\n );\n break;\n case 'game-details':\n let gameDetailsEvent = new EventResponse<StoreData | undefined>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n if (this.eventEmitter.listenerCount('game-details') === 0) {\n this.respondToMessage(\n message.id!!,\n {\n error: 'No event listener for game-details',\n },\n gameDetailsEvent\n );\n break;\n }\n this.eventEmitter.emit(\n 'game-details',\n message.args,\n gameDetailsEvent\n );\n const gameDetailsResult =\n await this.waitForEventToRespond(gameDetailsEvent);\n this.respondToMessage(\n message.id!!,\n gameDetailsResult.data,\n gameDetailsEvent\n );\n break;\n case 'check-for-updates':\n let checkForUpdatesEvent = new EventResponse<\n | {\n available: true;\n version: string;\n }\n | {\n available: false;\n }\n >((screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n this.eventEmitter.emit(\n 'check-for-updates',\n message.args,\n checkForUpdatesEvent\n );\n const checkForUpdatesResult =\n await this.waitForEventToRespond(checkForUpdatesEvent);\n this.respondToMessage(\n message.id!!,\n checkForUpdatesResult.data,\n checkForUpdatesEvent\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 let catalogEvent = new EventResponse<CatalogResponse>();\n this.eventEmitter.emit('catalog', catalogEvent);\n const catalogResult = await this.waitForEventToRespond(catalogEvent);\n this.respondToMessage(message.id!!, catalogResult.data, catalogEvent);\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 }\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 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.cjs","names":["pjson.version","events","Configuration","fs","EventResponse","Fuse","z","ConfigurationBuilder"],"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\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\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});\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/build/main.d.cts CHANGED
@@ -7,9 +7,19 @@ import { IFuseOptions } from "fuse.js";
7
7
  import { z } from "zod";
8
8
 
9
9
  //#region src/main.d.ts
10
- type OGIAddonEvent = 'connect' | 'disconnect' | 'configure' | 'authenticate' | 'search' | 'setup' | 'library-search' | 'game-details' | 'exit' | 'check-for-updates' | 'request-dl' | 'catalog';
10
+ /**
11
+ * Exposed events that the programmer can use to listen to and emit events.
12
+ */
13
+ type OGIAddonEvent = 'connect' | 'disconnect' | 'configure' | 'authenticate' | 'search' | 'setup' | 'library-search' | 'game-details' | 'exit' | 'check-for-updates' | 'request-dl' | 'catalog' | 'launch-app';
14
+ /**
15
+ * The events that the client can send to the server and are handled by the server.
16
+ */
11
17
  type OGIAddonClientSentEvent = 'response' | 'authenticate' | 'configure' | 'defer-update' | 'notification' | 'input-asked' | 'get-app-details' | 'search-app-name' | 'flag' | 'task-update';
12
- type OGIAddonServerSentEvent = 'authenticate' | 'configure' | 'config-update' | 'search' | 'setup' | 'response' | 'library-search' | 'check-for-updates' | 'task-run' | 'game-details' | 'request-dl' | 'catalog';
18
+ /**
19
+ * The events that the server sends to the client
20
+ * This is the events that the server can send to the client and are handled by the client.
21
+ */
22
+ type OGIAddonServerSentEvent = 'authenticate' | 'configure' | 'config-update' | 'launch-app' | 'search' | 'setup' | 'response' | 'library-search' | 'check-for-updates' | 'task-run' | 'game-details' | 'request-dl' | 'catalog';
13
23
  declare const VERSION: string;
14
24
  interface ClientSentEventTypes {
15
25
  response: any;
@@ -135,6 +145,7 @@ interface EventListenerTypes {
135
145
  type: 'direct' | 'torrent' | 'magnet' | 'empty';
136
146
  name: string;
137
147
  usedRealDebrid: boolean;
148
+ clearOldFilesBeforeUpdate?: boolean;
138
149
  multiPartFiles?: {
139
150
  name: string;
140
151
  downloadURL: string;
@@ -200,6 +211,16 @@ interface EventListenerTypes {
200
211
  } | {
201
212
  available: false;
202
213
  }>) => void;
214
+ /**
215
+ * 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.
216
+ * @param data {LibraryInfo} The library information for the app to be launched.
217
+ * @param launchType { 'pre' | 'post' } The type of launch task to perform.
218
+ * @param event {EventResponse<void>} The event response from the server.
219
+ */
220
+ 'launch-app': (data: {
221
+ libraryInfo: LibraryInfo;
222
+ launchType: 'pre' | 'post';
223
+ }, event: EventResponse<void>) => void;
203
224
  }
204
225
  interface StoreData {
205
226
  name: string;
@@ -463,8 +484,20 @@ declare class OGIAddonWSListener {
463
484
  addon: OGIAddon;
464
485
  constructor(ogiAddon: OGIAddon, eventEmitter: events.EventEmitter);
465
486
  private userInputAsked;
487
+ /**
488
+ * Registers the message receiver for the socket. This is used to receive messages from the server and handle them.
489
+ */
466
490
  private registerMessageReceiver;
467
491
  private waitForEventToRespond;
492
+ /**
493
+ * Common flow for events that use EventResponse with userInputAsked: create event, emit via callback, wait, respond.
494
+ * If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.
495
+ */
496
+ private handleEventWithResponse;
497
+ /**
498
+ * Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).
499
+ */
500
+ private handleEventWithResponseNoInput;
468
501
  respondToMessage(messageID: string, response: any, originalEvent: EventResponse<any> | undefined): void;
469
502
  waitForResponseFromServer<T>(messageID: string): Promise<T>;
470
503
  send(event: OGIAddonClientSentEvent, args: ClientSentEventTypes[OGIAddonClientSentEvent]): string;
package/build/main.d.mts CHANGED
@@ -7,9 +7,19 @@ import { z as z$1 } from "zod";
7
7
  import { IFuseOptions } from "fuse.js";
8
8
 
9
9
  //#region src/main.d.ts
10
- type OGIAddonEvent = 'connect' | 'disconnect' | 'configure' | 'authenticate' | 'search' | 'setup' | 'library-search' | 'game-details' | 'exit' | 'check-for-updates' | 'request-dl' | 'catalog';
10
+ /**
11
+ * Exposed events that the programmer can use to listen to and emit events.
12
+ */
13
+ type OGIAddonEvent = 'connect' | 'disconnect' | 'configure' | 'authenticate' | 'search' | 'setup' | 'library-search' | 'game-details' | 'exit' | 'check-for-updates' | 'request-dl' | 'catalog' | 'launch-app';
14
+ /**
15
+ * The events that the client can send to the server and are handled by the server.
16
+ */
11
17
  type OGIAddonClientSentEvent = 'response' | 'authenticate' | 'configure' | 'defer-update' | 'notification' | 'input-asked' | 'get-app-details' | 'search-app-name' | 'flag' | 'task-update';
12
- type OGIAddonServerSentEvent = 'authenticate' | 'configure' | 'config-update' | 'search' | 'setup' | 'response' | 'library-search' | 'check-for-updates' | 'task-run' | 'game-details' | 'request-dl' | 'catalog';
18
+ /**
19
+ * The events that the server sends to the client
20
+ * This is the events that the server can send to the client and are handled by the client.
21
+ */
22
+ type OGIAddonServerSentEvent = 'authenticate' | 'configure' | 'config-update' | 'launch-app' | 'search' | 'setup' | 'response' | 'library-search' | 'check-for-updates' | 'task-run' | 'game-details' | 'request-dl' | 'catalog';
13
23
  declare const VERSION: string;
14
24
  interface ClientSentEventTypes {
15
25
  response: any;
@@ -135,6 +145,7 @@ interface EventListenerTypes {
135
145
  type: 'direct' | 'torrent' | 'magnet' | 'empty';
136
146
  name: string;
137
147
  usedRealDebrid: boolean;
148
+ clearOldFilesBeforeUpdate?: boolean;
138
149
  multiPartFiles?: {
139
150
  name: string;
140
151
  downloadURL: string;
@@ -200,6 +211,16 @@ interface EventListenerTypes {
200
211
  } | {
201
212
  available: false;
202
213
  }>) => void;
214
+ /**
215
+ * 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.
216
+ * @param data {LibraryInfo} The library information for the app to be launched.
217
+ * @param launchType { 'pre' | 'post' } The type of launch task to perform.
218
+ * @param event {EventResponse<void>} The event response from the server.
219
+ */
220
+ 'launch-app': (data: {
221
+ libraryInfo: LibraryInfo;
222
+ launchType: 'pre' | 'post';
223
+ }, event: EventResponse<void>) => void;
203
224
  }
204
225
  interface StoreData {
205
226
  name: string;
@@ -463,8 +484,20 @@ declare class OGIAddonWSListener {
463
484
  addon: OGIAddon;
464
485
  constructor(ogiAddon: OGIAddon, eventEmitter: events.EventEmitter);
465
486
  private userInputAsked;
487
+ /**
488
+ * Registers the message receiver for the socket. This is used to receive messages from the server and handle them.
489
+ */
466
490
  private registerMessageReceiver;
467
491
  private waitForEventToRespond;
492
+ /**
493
+ * Common flow for events that use EventResponse with userInputAsked: create event, emit via callback, wait, respond.
494
+ * If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.
495
+ */
496
+ private handleEventWithResponse;
497
+ /**
498
+ * Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).
499
+ */
500
+ private handleEventWithResponseNoInput;
468
501
  respondToMessage(messageID: string, response: any, originalEvent: EventResponse<any> | undefined): void;
469
502
  waitForResponseFromServer<T>(messageID: string): Promise<T>;
470
503
  send(event: OGIAddonClientSentEvent, args: ClientSentEventTypes[OGIAddonClientSentEvent]): string;
package/build/main.mjs CHANGED
@@ -9,7 +9,7 @@ import { exec, spawn } from "node:child_process";
9
9
  import fs from "node:fs";
10
10
 
11
11
  //#region package.json
12
- var version = "2.3.0";
12
+ var version = "2.3.2";
13
13
 
14
14
  //#endregion
15
15
  //#region src/main.ts
@@ -435,6 +435,9 @@ var OGIAddonWSListener = class {
435
435
  }));
436
436
  return await this.waitForResponseFromServer(id);
437
437
  }
438
+ /**
439
+ * Registers the message receiver for the socket. This is used to receive messages from the server and handle them.
440
+ */
438
441
  registerMessageReceiver() {
439
442
  this.socket.on("message", async (data) => {
440
443
  const message = JSON.parse(data);
@@ -448,10 +451,7 @@ var OGIAddonWSListener = class {
448
451
  else this.respondToMessage(message.id, { success: true }, void 0);
449
452
  break;
450
453
  case "search":
451
- let searchResultEvent = new EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
452
- this.eventEmitter.emit("search", message.args, searchResultEvent);
453
- const searchResult = await this.waitForEventToRespond(searchResultEvent);
454
- this.respondToMessage(message.id, searchResult.data, searchResultEvent);
454
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("search", message.args, event));
455
455
  break;
456
456
  case "setup": {
457
457
  let setupEvent = new EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
@@ -473,26 +473,16 @@ var OGIAddonWSListener = class {
473
473
  break;
474
474
  }
475
475
  case "library-search":
476
- let librarySearchEvent = new EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
477
- this.eventEmitter.emit("library-search", message.args, librarySearchEvent);
478
- const librarySearchResult = await this.waitForEventToRespond(librarySearchEvent);
479
- this.respondToMessage(message.id, librarySearchResult.data, librarySearchEvent);
476
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("library-search", message.args, event));
480
477
  break;
481
478
  case "game-details":
482
- let gameDetailsEvent = new EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
483
- if (this.eventEmitter.listenerCount("game-details") === 0) {
484
- this.respondToMessage(message.id, { error: "No event listener for game-details" }, gameDetailsEvent);
485
- break;
486
- }
487
- this.eventEmitter.emit("game-details", message.args, gameDetailsEvent);
488
- const gameDetailsResult = await this.waitForEventToRespond(gameDetailsEvent);
489
- this.respondToMessage(message.id, gameDetailsResult.data, gameDetailsEvent);
479
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("game-details", message.args, event), {
480
+ requireListener: "game-details",
481
+ noListenerError: "No event listener for game-details"
482
+ });
490
483
  break;
491
484
  case "check-for-updates":
492
- let checkForUpdatesEvent = new EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
493
- this.eventEmitter.emit("check-for-updates", message.args, checkForUpdatesEvent);
494
- const checkForUpdatesResult = await this.waitForEventToRespond(checkForUpdatesEvent);
495
- this.respondToMessage(message.id, checkForUpdatesResult.data, checkForUpdatesEvent);
485
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("check-for-updates", message.args, event));
496
486
  break;
497
487
  case "request-dl":
498
488
  let requestDLEvent = new EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
@@ -510,10 +500,7 @@ var OGIAddonWSListener = class {
510
500
  this.respondToMessage(message.id, requestDLResult.data, requestDLEvent);
511
501
  break;
512
502
  case "catalog":
513
- let catalogEvent = new EventResponse();
514
- this.eventEmitter.emit("catalog", catalogEvent);
515
- const catalogResult = await this.waitForEventToRespond(catalogEvent);
516
- this.respondToMessage(message.id, catalogResult.data, catalogEvent);
503
+ await this.handleEventWithResponseNoInput(message, (event) => this.eventEmitter.emit("catalog", event));
517
504
  break;
518
505
  case "task-run": {
519
506
  let taskRunEvent = new EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
@@ -550,6 +537,9 @@ var OGIAddonWSListener = class {
550
537
  this.respondToMessage(message.id, taskRunResult.data, taskRunEvent);
551
538
  break;
552
539
  }
540
+ case "launch-app":
541
+ await this.handleEventWithResponse(message, (event) => this.eventEmitter.emit("launch-app", message.args, event));
542
+ break;
553
543
  }
554
544
  });
555
545
  }
@@ -574,6 +564,29 @@ var OGIAddonWSListener = class {
574
564
  }, 5e3);
575
565
  });
576
566
  }
567
+ /**
568
+ * Common flow for events that use EventResponse with userInputAsked: create event, emit via callback, wait, respond.
569
+ * If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.
570
+ */
571
+ async handleEventWithResponse(message, emit, options) {
572
+ const event = new EventResponse((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
573
+ if (options && this.eventEmitter.listenerCount(options.requireListener) === 0) {
574
+ this.respondToMessage(message.id, { error: options.noListenerError }, event);
575
+ return;
576
+ }
577
+ emit(event);
578
+ const result = await this.waitForEventToRespond(event);
579
+ this.respondToMessage(message.id, result.data, event);
580
+ }
581
+ /**
582
+ * Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).
583
+ */
584
+ async handleEventWithResponseNoInput(message, emit) {
585
+ const event = new EventResponse();
586
+ emit(event);
587
+ const result = await this.waitForEventToRespond(event);
588
+ this.respondToMessage(message.id, result.data, event);
589
+ }
577
590
  respondToMessage(messageID, response, originalEvent) {
578
591
  this.socket.send(JSON.stringify({
579
592
  event: "response",
@@ -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\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\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\nexport type OGIAddonServerSentEvent =\n | 'authenticate'\n | 'configure'\n | 'config-update'\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 = Record<string, CatalogSection> | CatalogWithCarousel;\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\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 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: (\n event: Omit<\n EventResponse<CatalogResponse>,\n 'askForInput'\n >\n ) => 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\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});\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 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 let searchResultEvent = new EventResponse<SearchResult[]>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n this.eventEmitter.emit('search', message.args, searchResultEvent);\n const searchResult =\n await this.waitForEventToRespond(searchResultEvent);\n this.respondToMessage(\n message.id!!,\n searchResult.data,\n searchResultEvent\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 let librarySearchEvent = new EventResponse<BasicLibraryInfo[]>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n this.eventEmitter.emit(\n 'library-search',\n message.args,\n librarySearchEvent\n );\n const librarySearchResult =\n await this.waitForEventToRespond(librarySearchEvent);\n this.respondToMessage(\n message.id!!,\n librarySearchResult.data,\n librarySearchEvent\n );\n break;\n case 'game-details':\n let gameDetailsEvent = new EventResponse<StoreData | undefined>(\n (screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n if (this.eventEmitter.listenerCount('game-details') === 0) {\n this.respondToMessage(\n message.id!!,\n {\n error: 'No event listener for game-details',\n },\n gameDetailsEvent\n );\n break;\n }\n this.eventEmitter.emit(\n 'game-details',\n message.args,\n gameDetailsEvent\n );\n const gameDetailsResult =\n await this.waitForEventToRespond(gameDetailsEvent);\n this.respondToMessage(\n message.id!!,\n gameDetailsResult.data,\n gameDetailsEvent\n );\n break;\n case 'check-for-updates':\n let checkForUpdatesEvent = new EventResponse<\n | {\n available: true;\n version: string;\n }\n | {\n available: false;\n }\n >((screen, name, description) =>\n this.userInputAsked(screen, name, description, this.socket)\n );\n this.eventEmitter.emit(\n 'check-for-updates',\n message.args,\n checkForUpdatesEvent\n );\n const checkForUpdatesResult =\n await this.waitForEventToRespond(checkForUpdatesEvent);\n this.respondToMessage(\n message.id!!,\n checkForUpdatesResult.data,\n checkForUpdatesEvent\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 let catalogEvent = new EventResponse<CatalogResponse>();\n this.eventEmitter.emit('catalog', catalogEvent);\n const catalogResult = await this.waitForEventToRespond(catalogEvent);\n this.respondToMessage(message.id!!, catalogResult.data, catalogEvent);\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 }\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 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 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\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\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});\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.3.0",
6
+ "version": "2.3.2",
7
7
  "exports": {
8
8
  ".": {
9
9
  "import": {
@@ -1,6 +1,12 @@
1
1
  type BaseRequiredFields = {
2
2
  name: string;
3
3
  manifest?: Record<string, any>;
4
+ /**
5
+ * Update-only hint for OGI's setup stage.
6
+ * When false, OGI keeps existing files in place instead of moving them to `old_files`
7
+ * before running update setup. Defaults to true/undefined behavior.
8
+ */
9
+ clearOldFilesBeforeUpdate?: boolean;
4
10
  };
5
11
 
6
12
  export type SearchResult = BaseRequiredFields &
package/src/main.ts CHANGED
@@ -7,6 +7,9 @@ import EventResponse from './EventResponse';
7
7
  import type { SearchResult } from './SearchEngine';
8
8
  import Fuse, { IFuseOptions } from 'fuse.js';
9
9
 
10
+ /**
11
+ * Exposed events that the programmer can use to listen to and emit events.
12
+ */
10
13
  export type OGIAddonEvent =
11
14
  | 'connect'
12
15
  | 'disconnect'
@@ -19,8 +22,12 @@ export type OGIAddonEvent =
19
22
  | 'exit'
20
23
  | 'check-for-updates'
21
24
  | 'request-dl'
22
- | 'catalog';
25
+ | 'catalog'
26
+ | 'launch-app';
23
27
 
28
+ /**
29
+ * The events that the client can send to the server and are handled by the server.
30
+ */
24
31
  export type OGIAddonClientSentEvent =
25
32
  | 'response'
26
33
  | 'authenticate'
@@ -33,10 +40,15 @@ export type OGIAddonClientSentEvent =
33
40
  | 'flag'
34
41
  | 'task-update';
35
42
 
43
+ /**
44
+ * The events that the server sends to the client
45
+ * This is the events that the server can send to the client and are handled by the client.
46
+ */
36
47
  export type OGIAddonServerSentEvent =
37
48
  | 'authenticate'
38
49
  | 'configure'
39
50
  | 'config-update'
51
+ | 'launch-app'
40
52
  | 'search'
41
53
  | 'setup'
42
54
  | 'response'
@@ -122,7 +134,9 @@ export interface CatalogWithCarousel {
122
134
  carousel?: Record<string, CatalogCarouselItem> | CatalogCarouselItem[];
123
135
  }
124
136
 
125
- export type CatalogResponse = Record<string, CatalogSection> | CatalogWithCarousel;
137
+ export type CatalogResponse =
138
+ | Record<string, CatalogSection>
139
+ | CatalogWithCarousel;
126
140
 
127
141
  export type SetupEventResponse = Omit<
128
142
  LibraryInfo,
@@ -206,6 +220,7 @@ export interface EventListenerTypes {
206
220
  type: 'direct' | 'torrent' | 'magnet' | 'empty';
207
221
  name: string;
208
222
  usedRealDebrid: boolean;
223
+ clearOldFilesBeforeUpdate?: boolean;
209
224
  multiPartFiles?: {
210
225
  name: string;
211
226
  downloadURL: string;
@@ -271,12 +286,7 @@ export interface EventListenerTypes {
271
286
  * @param event
272
287
  * @returns
273
288
  */
274
- catalog: (
275
- event: Omit<
276
- EventResponse<CatalogResponse>,
277
- 'askForInput'
278
- >
279
- ) => void;
289
+ catalog: (event: Omit<EventResponse<CatalogResponse>, 'askForInput'>) => void;
280
290
 
281
291
  /**
282
292
  * This event is emitted when the client requests for an addon to check for updates. Addon should resolve the event with the update information.
@@ -296,6 +306,17 @@ export interface EventListenerTypes {
296
306
  }
297
307
  >
298
308
  ) => void;
309
+
310
+ /**
311
+ * 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.
312
+ * @param data {LibraryInfo} The library information for the app to be launched.
313
+ * @param launchType { 'pre' | 'post' } The type of launch task to perform.
314
+ * @param event {EventResponse<void>} The event response from the server.
315
+ */
316
+ 'launch-app': (
317
+ data: { libraryInfo: LibraryInfo; launchType: 'pre' | 'post' },
318
+ event: EventResponse<void>
319
+ ) => void;
299
320
  }
300
321
 
301
322
  export interface StoreData {
@@ -962,6 +983,9 @@ class OGIAddonWSListener {
962
983
  return await this.waitForResponseFromServer<U>(id);
963
984
  }
964
985
 
986
+ /**
987
+ * Registers the message receiver for the socket. This is used to receive messages from the server and handle them.
988
+ */
965
989
  private registerMessageReceiver() {
966
990
  this.socket.on('message', async (data: string) => {
967
991
  const message: WebsocketMessageServer = JSON.parse(data);
@@ -982,17 +1006,8 @@ class OGIAddonWSListener {
982
1006
  }
983
1007
  break;
984
1008
  case 'search':
985
- let searchResultEvent = new EventResponse<SearchResult[]>(
986
- (screen, name, description) =>
987
- this.userInputAsked(screen, name, description, this.socket)
988
- );
989
- this.eventEmitter.emit('search', message.args, searchResultEvent);
990
- const searchResult =
991
- await this.waitForEventToRespond(searchResultEvent);
992
- this.respondToMessage(
993
- message.id!!,
994
- searchResult.data,
995
- searchResultEvent
1009
+ await this.handleEventWithResponse<SearchResult[]>(message, (event) =>
1010
+ this.eventEmitter.emit('search', message.args, event)
996
1011
  );
997
1012
  break;
998
1013
  case 'setup': {
@@ -1018,74 +1033,28 @@ class OGIAddonWSListener {
1018
1033
  break;
1019
1034
  }
1020
1035
  case 'library-search':
1021
- let librarySearchEvent = new EventResponse<BasicLibraryInfo[]>(
1022
- (screen, name, description) =>
1023
- this.userInputAsked(screen, name, description, this.socket)
1024
- );
1025
- this.eventEmitter.emit(
1026
- 'library-search',
1027
- message.args,
1028
- librarySearchEvent
1029
- );
1030
- const librarySearchResult =
1031
- await this.waitForEventToRespond(librarySearchEvent);
1032
- this.respondToMessage(
1033
- message.id!!,
1034
- librarySearchResult.data,
1035
- librarySearchEvent
1036
+ await this.handleEventWithResponse<BasicLibraryInfo[]>(
1037
+ message,
1038
+ (event) =>
1039
+ this.eventEmitter.emit('library-search', message.args, event)
1036
1040
  );
1037
1041
  break;
1038
1042
  case 'game-details':
1039
- let gameDetailsEvent = new EventResponse<StoreData | undefined>(
1040
- (screen, name, description) =>
1041
- this.userInputAsked(screen, name, description, this.socket)
1042
- );
1043
- if (this.eventEmitter.listenerCount('game-details') === 0) {
1044
- this.respondToMessage(
1045
- message.id!!,
1046
- {
1047
- error: 'No event listener for game-details',
1048
- },
1049
- gameDetailsEvent
1050
- );
1051
- break;
1052
- }
1053
- this.eventEmitter.emit(
1054
- 'game-details',
1055
- message.args,
1056
- gameDetailsEvent
1057
- );
1058
- const gameDetailsResult =
1059
- await this.waitForEventToRespond(gameDetailsEvent);
1060
- this.respondToMessage(
1061
- message.id!!,
1062
- gameDetailsResult.data,
1063
- gameDetailsEvent
1043
+ await this.handleEventWithResponse<StoreData | undefined>(
1044
+ message,
1045
+ (event) =>
1046
+ this.eventEmitter.emit('game-details', message.args, event),
1047
+ {
1048
+ requireListener: 'game-details',
1049
+ noListenerError: 'No event listener for game-details',
1050
+ }
1064
1051
  );
1065
1052
  break;
1066
1053
  case 'check-for-updates':
1067
- let checkForUpdatesEvent = new EventResponse<
1068
- | {
1069
- available: true;
1070
- version: string;
1071
- }
1072
- | {
1073
- available: false;
1074
- }
1075
- >((screen, name, description) =>
1076
- this.userInputAsked(screen, name, description, this.socket)
1077
- );
1078
- this.eventEmitter.emit(
1079
- 'check-for-updates',
1080
- message.args,
1081
- checkForUpdatesEvent
1082
- );
1083
- const checkForUpdatesResult =
1084
- await this.waitForEventToRespond(checkForUpdatesEvent);
1085
- this.respondToMessage(
1086
- message.id!!,
1087
- checkForUpdatesResult.data,
1088
- checkForUpdatesEvent
1054
+ await this.handleEventWithResponse<
1055
+ { available: true; version: string } | { available: false }
1056
+ >(message, (event) =>
1057
+ this.eventEmitter.emit('check-for-updates', message.args, event)
1089
1058
  );
1090
1059
  break;
1091
1060
  case 'request-dl':
@@ -1130,10 +1099,10 @@ class OGIAddonWSListener {
1130
1099
  );
1131
1100
  break;
1132
1101
  case 'catalog':
1133
- let catalogEvent = new EventResponse<CatalogResponse>();
1134
- this.eventEmitter.emit('catalog', catalogEvent);
1135
- const catalogResult = await this.waitForEventToRespond(catalogEvent);
1136
- this.respondToMessage(message.id!!, catalogResult.data, catalogEvent);
1102
+ await this.handleEventWithResponseNoInput<CatalogResponse>(
1103
+ message,
1104
+ (event) => this.eventEmitter.emit('catalog', event)
1105
+ );
1137
1106
  break;
1138
1107
  case 'task-run': {
1139
1108
  let taskRunEvent = new EventResponse<void>(
@@ -1201,6 +1170,11 @@ class OGIAddonWSListener {
1201
1170
  this.respondToMessage(message.id!!, taskRunResult.data, taskRunEvent);
1202
1171
  break;
1203
1172
  }
1173
+ case 'launch-app':
1174
+ await this.handleEventWithResponse<void>(message, (event) =>
1175
+ this.eventEmitter.emit('launch-app', message.args, event)
1176
+ );
1177
+ break;
1204
1178
  }
1205
1179
  });
1206
1180
  }
@@ -1233,6 +1207,47 @@ class OGIAddonWSListener {
1233
1207
  });
1234
1208
  }
1235
1209
 
1210
+ /**
1211
+ * Common flow for events that use EventResponse with userInputAsked: create event, emit via callback, wait, respond.
1212
+ * If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.
1213
+ */
1214
+ private async handleEventWithResponse<T>(
1215
+ message: WebsocketMessageServer,
1216
+ emit: (event: EventResponse<T>) => void,
1217
+ options?: { requireListener: string; noListenerError: string }
1218
+ ): Promise<void> {
1219
+ const event = new EventResponse<T>((screen, name, description) =>
1220
+ this.userInputAsked(screen, name, description, this.socket)
1221
+ );
1222
+ if (
1223
+ options &&
1224
+ this.eventEmitter.listenerCount(options.requireListener) === 0
1225
+ ) {
1226
+ this.respondToMessage(
1227
+ message.id!!,
1228
+ { error: options.noListenerError },
1229
+ event
1230
+ );
1231
+ return;
1232
+ }
1233
+ emit(event);
1234
+ const result = await this.waitForEventToRespond(event);
1235
+ this.respondToMessage(message.id!!, result.data, event);
1236
+ }
1237
+
1238
+ /**
1239
+ * Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).
1240
+ */
1241
+ private async handleEventWithResponseNoInput<T>(
1242
+ message: WebsocketMessageServer,
1243
+ emit: (event: EventResponse<T>) => void
1244
+ ): Promise<void> {
1245
+ const event = new EventResponse<T>();
1246
+ emit(event);
1247
+ const result = await this.waitForEventToRespond(event);
1248
+ this.respondToMessage(message.id!!, result.data, event);
1249
+ }
1250
+
1236
1251
  public respondToMessage(
1237
1252
  messageID: string,
1238
1253
  response: any,