diff --git a/api/.env.development b/api/.env.development index 3261fcf359..35a4b30d71 100644 --- a/api/.env.development +++ b/api/.env.development @@ -32,3 +32,4 @@ CHOKIDAR_USEPOLLING=true LOG_TRANSPORT=console LOG_LEVEL=trace ENABLE_NEXT_DOCKER_RELEASE=true +SKIP_CONNECT_PLUGIN_CHECK=true diff --git a/packages/unraid-api-plugin-connect/src/index.ts b/packages/unraid-api-plugin-connect/src/index.ts index 21748673f7..d799da9765 100644 --- a/packages/unraid-api-plugin-connect/src/index.ts +++ b/packages/unraid-api-plugin-connect/src/index.ts @@ -1,5 +1,8 @@ import { Inject, Logger, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { existsSync } from 'node:fs'; + +import { execa } from 'execa'; import { ConnectConfigPersister } from './config/config.persistence.js'; import { configFeature } from './config/connect.config.js'; @@ -8,6 +11,10 @@ import { ConnectModule } from './unraid-connect/connect.module.js'; export const adapter = 'nestjs'; +/** + * When the plugin is installed we expose the full Nest module graph. + * Configuration and proxy submodules only bootstrap in this branch. + */ @Module({ imports: [ConfigModule.forFeature(configFeature), ConnectModule, MothershipModule], providers: [ConnectConfigPersister], @@ -23,4 +30,64 @@ class ConnectPluginModule { } } -export const ApiModule = ConnectPluginModule; +/** + * Fallback module keeps the export shape intact but only warns operators. + * This makes `ApiModule` safe to import even when the plugin is absent. + */ +@Module({}) +export class DisabledConnectPluginModule { + logger = new Logger(DisabledConnectPluginModule.name); + async onModuleInit() { + const removalCommand = 'unraid-api plugins remove -b unraid-api-plugin-connect'; + + this.logger.warn( + 'Connect plugin is not installed, but is listed as an API plugin. Attempting `%s` automatically.', + removalCommand + ); + + try { + const { stdout, stderr } = await execa('unraid-api', [ + 'plugins', + 'remove', + '-b', + 'unraid-api-plugin-connect', + ]); + + if (stdout?.trim()) { + this.logger.debug(stdout.trim()); + } + + if (stderr?.trim()) { + this.logger.debug(stderr.trim()); + } + + this.logger.log( + 'Successfully completed `%s` to prune the stale connect plugin entry.', + removalCommand + ); + } catch (error) { + const message = + error instanceof Error + ? error.message + : 'Unknown error while removing stale connect plugin entry.'; + this.logger.error('Failed to run `%s`: %s', removalCommand, message); + } + } +} + +/** + * Local filesystem and env checks stay synchronous so we can branch at module load. + */ +const isConnectPluginInstalled = () => { + if (process.env.SKIP_CONNECT_PLUGIN_CHECK === 'true') { + return true; + } + return existsSync('/boot/config/plugins/dynamix.unraid.net.plg'); +}; + +/** + * Downstream code always imports `ApiModule`. We swap the implementation based on availability, + * avoiding dynamic module plumbing while keeping the DI graph predictable. + * Set `SKIP_CONNECT_PLUGIN_CHECK=true` in development to force the connected path. + */ +export const ApiModule = isConnectPluginInstalled() ? ConnectPluginModule : DisabledConnectPluginModule;