HyperHQ Plugin Developer Guide
This guide shows how to build plugins for the current HyperHQ plugin system.
For full event names and method details, keep the Plugin API Reference and Socket.IO Guide nearby.
What a plugin is
A HyperHQ plugin is a folder under the configured HyperSpin installation's plugins/ directory. HyperHQ discovers plugins by looking for a plugin.json file in each plugin folder.
Plugins can be:
executable: a separate process communicating through stdio or Socket.IO.javascript: JavaScript loaded by HyperHQ's sandbox runner.
Minimal folder layout
my-plugin/
├── plugin.json # Required; this is the manifest HyperHQ loads
├── plugin.exe # Executable plugin entry point on Windows
├── plugin # Optional Linux/macOS executable
├── icon.png # Optional icon
└── assets/ # Optional packaged files
Do not name the manifest manifest.json; plugin discovery and validation use plugin.json.
Minimal executable manifest
{
"id": "system-info",
"name": "System Info",
"version": "1.0.0",
"description": "Displays basic system information",
"author": "Your Name",
"type": "executable",
"executable": "plugin.exe",
"executableProviders": {
"windows": "plugin.exe",
"linux": "plugin",
"macos": "plugin"
},
"communication": {
"preferred": "stdio",
"fallback": "socketio",
"stdio": { "enabled": true },
"socketio": { "enabled": true }
},
"capabilities": [
{ "name": "system-info", "description": "Reports system information", "required": true }
],
"permissions": [],
"settings": [
{
"key": "refreshInterval",
"type": "number",
"label": "Refresh Interval (seconds)",
"defaultValue": 5,
"validation": { "min": 1, "max": 60 }
},
{
"key": "showDiskUsage",
"type": "boolean",
"label": "Show Disk Usage",
"defaultValue": true
}
]
}
Important schema details:
typeis required:executableorjavascript.capabilitiesis required and must not be empty.permissionsis an array of{ type, scope, description }objects.- Settings use
label, notname. - Current setting types include
string,number,boolean,email,password,url,textarea,select,oauth,array,object,file,directory,color, andrange. - Prefer
executableProvidersfor platform-specific binaries.executableLinuxandexecutableMacremain supported as legacy compatibility fields.
Communication modes
Stdio
Stdio plugins receive one JSON request per line and should print one JSON response per line.
{"id":"1","type":"request","method":"initialize","data":{"settings":{}},"timestamp":1710000000000}
Response:
{"id":"1","type":"response","data":"initialized","timestamp":1710000000100}
Socket.IO
Configure Socket.IO under communication, connect to http://localhost:${HYPERHQ_SOCKET_PORT}, and authenticate with the launch challenge. There is no /plugin namespace.
After authentication, subscribe to broadcasts with subscribeEvents and listen for hyperHqEvent.
Socket.IO plugins receive these environment variables:
HYPERHQ_PLUGIN_IDHYPERHQ_PLUGIN_NAMEHYPERHQ_PLUGIN_VERSIONHYPERHQ_SOCKET_PORTHYPERHQ_AUTH_CHALLENGEPLUGIN_SETTINGS(compatibility only; read current settings frominitialize(data).settings)
The auth challenge is one-time-use and expires after 30 seconds. Reconnect within the 30-second grace period with authenticate({ pluginId, sessionToken }).
See Plugin Socket.IO Guide for complete examples.
Required methods
Every plugin must support:
| Method | Required | Description |
|---|---|---|
initialize(data) | Yes | Receive settings and plugin metadata. |
execute(data) | Yes | Run an action or the main plugin operation. |
test(data) | Yes | Health check. |
shutdown(data) | Optional | Cleanup before stopping. |
For stdio plugins that emit a complete PluginResponse, startup setup can be reported with top-level status: "requiresConfig" and a clear message. Socket.IO and JavaScript plugins should return a clear initialization error/action result until top-level status propagation is supported on those paths.
{
"id": "init-1",
"type": "response",
"status": "requiresConfig",
"message": "Connect your account before using this plugin.",
"data": null
}
Python stdio example
#!/usr/bin/env python3
import json
import platform
import sys
import time
class Plugin:
def __init__(self):
self.settings = {}
def initialize(self, data):
self.settings = data.get('settings', {})
return 'initialized'
def execute(self, data):
return {
'system': platform.system(),
'release': platform.release(),
'action': data.get('action', 'get_info')
}
def test(self, data):
return True
def shutdown(self, data):
return 'ok'
plugin = Plugin()
for line in sys.stdin:
if not line.strip():
continue
message = json.loads(line)
method = message.get('method')
request_id = message.get('id')
data = message.get('data') or {}
try:
if method == 'initialize':
result = plugin.initialize(data)
elif method == 'execute':
result = plugin.execute(data)
elif method == 'test':
result = plugin.test(data)
elif method == 'shutdown':
result = plugin.shutdown(data)
else:
raise ValueError(f'Unknown method: {method}')
print(json.dumps({
'id': request_id,
'type': 'response',
'data': result,
'timestamp': int(time.time() * 1000)
}), flush=True)
except Exception as exc:
print(json.dumps({
'id': request_id,
'type': 'error',
'error': {'message': str(exc), 'recoverable': True},
'timestamp': int(time.time() * 1000)
}), flush=True)
JavaScript plugin example
The current JavaScript runner creates a sandbox global named plugin. Define plugin.initialize, plugin.execute, plugin.test, and optionally plugin.shutdown. Do not use old module.exports, onLoad, or hook-style examples.
plugin.json:
{
"id": "js-hello",
"name": "JS Hello",
"version": "1.0.0",
"description": "JavaScript plugin example",
"author": "Your Name",
"type": "javascript",
"main": "plugin.js",
"capabilities": [
{ "name": "hello", "description": "Says hello", "required": true }
],
"permissions": []
}
plugin.js:
plugin.initialize = async (data) => {
plugin._settings = data.settings || {};
return 'initialized';
};
plugin.execute = async (data) => {
return { message: `Hello ${data.name || 'HyperHQ'}` };
};
plugin.test = async () => true;
plugin.shutdown = async () => 'ok';
Settings, OAuth, and onboarding
Settings are top-level manifest entries. HyperHQ renders current setting types including OAuth, structured fields, and path pickers.
Path picker settings:
- Use
type: "file"for file paths. - Use
type: "directory"for folder paths. - Add
browse: "file"orbrowse: "directory"to astringsetting when users need editable text plus a path picker. - Use
browse: { "type": "directory", "buttonLabel": "Choose Folder" }to set the picker mode and button text. - Use
validation.fileTypesto filter file pickers.
{
"settings": [
{
"key": "account",
"type": "oauth",
"label": "Account",
"oauth": {
"provider": "example",
"authUrl": "https://example.com/oauth/authorize",
"callbackUrls": ["https://example.com/oauth/callback"],
"requiredCallbackParams": ["code"],
"statusAction": "oauth.status"
}
},
{
"key": "libraryRoots",
"type": "array",
"label": "Library Roots",
"validation": { "itemType": "string", "minItems": 1 }
},
{
"key": "romFolder",
"type": "string",
"label": "ROM Folder",
"browse": "directory",
"defaultValue": "",
"description": "Folder containing ROM files"
},
{
"key": "emulatorExe",
"type": "file",
"label": "Emulator Executable",
"validation": {
"fileTypes": [
{ "name": "Executables", "extensions": ["exe"] }
]
}
}
]
}
Onboarding form steps use the same setting shape, including file, directory, browse, and validation.fileTypes.
Use manifest-driven onboarding wizards to guide first-run setup:
{
"actions": [
{
"id": "setup",
"label": "Setup",
"description": "Configure this plugin",
"icon": "settings",
"type": "wizard",
"wizard_id": "first-run"
}
],
"onboarding": {
"wizards": [
{
"id": "first-run",
"title": "Set up the plugin",
"autoStart": "first-run",
"skippable": false,
"steps": [
{ "id": "intro", "type": "info", "title": "Welcome", "description": "Let's configure this plugin." },
{ "id": "login", "type": "oauth", "title": "Connect Account", "oauth": { "provider": "example", "settingKey": "account" } },
{ "id": "options", "type": "form", "title": "Options", "form": [ { "key": "enabled", "type": "boolean", "label": "Enable sync", "defaultValue": true } ] }
]
}
]
}
}
Wizard step types are info, oauth, action, async-action, selection-list, and form.
Calling HyperHQ from Socket.IO plugins
Data requests are sent with requestData or request_data; HyperHQ replies with both dataResponse and data_response for compatibility.
socket.emit('requestData', {
method: 'getSystems',
params: {},
requestId: 'systems-1',
sessionToken
});
Supported data methods:
| Method | Behavior |
|---|---|
getSystems | Blocking. |
getMediaFolders | Blocking. |
getGamesForSystem | Blocking; system identifier can be ID/reference/name where frontend supports it. |
createSystem | Fire-and-forget. |
createEmulator | Fire-and-forget. |
addGames | Fire-and-forget; accepts broader game metadata payloads. |
removeGames | Fire-and-forget; removal aliases are handled by the frontend integration. |
launchGame | Fire-and-forget. |
Packaged file access
Socket.IO plugins can use requestFile to read packaged files from their own plugin directory. The request must include the session token, the resolved path must remain inside the plugin directory, and the file must be 10 MB or smaller.
Storage and installation
- Installed plugins live under the configured HyperSpin installation's
plugins/folder. - Plugin cached data and downloaded assets should use
_hsm/Plugins/{PluginName}/. - Do not document or rely on a
%PLUGIN_DATA%magic path; use real paths from settings or HyperHQ APIs.
Leaderboard provider plugins
To provide leaderboard/score functionality, advertise:
{
"capabilities": [
{ "name": "leaderboard-provider", "description": "Provides leaderboard data", "required": false }
]
}
Implement these provider methods when applicable:
leaderboard.getProviderMetadataleaderboard.getLeaderboardleaderboard.getScoreleaderboard.submitScore
Packaging and marketplace notes
Package plugins as ZIP files containing plugin.json, runtime files, icons, and docs. Marketplace version records can be channel- and platform-specific, so plan release artifacts accordingly (for example stable Windows and beta Linux builds as separate versions).
There are no checked-in starter template ZIPs in this docs folder. Treat examples in these docs as conceptual starting points rather than download links.
Troubleshooting and debugging
- Log incoming request methods and outgoing response IDs while developing.
- Never log OAuth tokens, API keys, auth challenges, or session tokens.
- Confirm
plugin.jsonis named correctly and contains requiredtype,capabilities, andpermissionsfields. - For executable plugins, confirm platform executable paths and file permissions.
- For stdio plugins, write only JSON response lines to stdout. Send debug logs to stderr.
- For Socket.IO plugins, connect to the root namespace, read
HYPERHQ_SOCKET_PORT, and authenticate withHYPERHQ_AUTH_CHALLENGE. - For Socket.IO responses, echo the incoming request
idexactly. - Return useful errors from
initializewhen settings are missing, especially for OAuth and onboarding flows.
Manual test commands
For stdio plugins:
printf '%s\n' '{"id":"1","type":"request","method":"initialize","data":{"settings":{}},"timestamp":0}' | ./plugin
printf '%s\n' '{"id":"2","type":"request","method":"execute","data":{"action":"test"},"timestamp":0}' | ./plugin
printf '%s\n' '{"id":"3","type":"request","method":"test","data":{},"timestamp":0}' | ./plugin
For Socket.IO plugins, launch through HyperHQ so the required port, challenge, and settings environment variables are present.