Plugin API reference
Plugin scripts interact with the launcher through the global objectwindow.__PLUGIN_API__. This page lists every callable method, its signature, required permissions, and examples.
Global variables
PLUGIN_API_BASE — plugin file URL prefix
Inside the L2 Sandbox (Sandbox), the launcher injectswindow.__PLUGIN_API_BASE__ with the file access base URL of the current plugin:
l2) or in Overlay windows (Overlay window)(overlay iframe); resolve relative paths another way there.
How to call
Permission model
Every API method maps to one permission. On each call the launcher checks whether the plugin manifest’spermissions contains that permission; otherwise it errors:
Full permission list
The risk level drives the colour hint in the install details dialog: Normal = blue, Warning = yellow, Dangerous = red. Follow the principle of least privilege and request only the permissions you actually use.
Method reference
getSettings — Read plugin settings
Read the plugin’s ownsettings.json.
- Permission:
config:read - Returns:
Record<string, unknown>({}when the file does not exist) - Storage:
{dataDir}/plugins/{pluginId}/settings.json
setSettings — Write plugin settings
Merge settings by key (merge, not replace the whole file).- Permission:
config:write - Parameters:
(key: string, value: any); value can be any JSON value - Concurrent writes are protected by a backend lock
setCache / getCache — Plugin cache
Read and write the plugin’s own cache file by key, with TTL expiry.- Permission:
cache:access - Storage:
{dataDir}/plugins/{pluginId}/cache.json - Internal shape is
{ key: { v: value, e: expiresAt|null } }; plugins do not need to care about it
callBackend — Call the launcher backend API
Call the launcher backend directly (http://localhost:5000/api/...), bypassing CORS restrictions.
- Permission:
network:fetch - Parameters:
(endpoint: string, data?: any) - With
data→ POST; withoutdata→ GET - You can pass a
_methodfield in thedataobject to override the HTTP method (for example{ _method: 'PUT', State: 'active' });_methodis not sent to the backend endpointstarts with/(such as/instance) and excludes the/apiprefix- Returns: the backend JSON response; throws on non-2xx
- For the list of endpoints, see the launcher backend OpenAPI (dev mode
/openapi/v1.json); common ones include/instance,/resources/search,/settings, and so on
sandbox="allow-scripts", no allow-popups), window.open(url, '_blank') is blocked. To open external links, use the backend endpoint:
http/https links in the system default browser (only these two schemes are accepted; anything else returns 400).
proxyFetch — CORS proxy request
Forward a request to any external URL through the launcher backend, bypassing browser CORS restrictions, with SSRF protection.- Permission:
network:cors_proxy - Request
ProxyRequest:
- Response
ProxyResponse:
- SSRF protection: only http/https; private and reserved addresses are rejected (localhost, 127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x, and so on) with
PROXY_PRIVATE_ADDRESS(400)
proxyFetchStream — Streaming proxy request
Consume an SSE stream (such as word-by-word AI output) with a callback per chunk.- Permission:
network:cors_proxy - The backend automatically adds
stream: truewhen forwarding the upstream stream signal(AbortSignal) aborts the stream;req.signalis stripped automatically before postMessage and sent as a__plugin_api_abortmessage instead- ⚠️ Known limitation: the Promise returned by
proxyFetchStreamresolves when the stream ends, but chunk callbacks may still fire after it resolves. UseonErroror a[DONE]marker within the stream to detect completion
registerMethod — Register a plugin method
Register a method of the current plugin in the global registry so that other plugins can call it viacallPlugin.
- Permission:
config:write - Parameters:
(method: string, fn: Function);fnmay return a value or a Promise - All methods are unregistered automatically when the plugin is disabled
- See Plugin dependencies and inter-plugin calls for details
callPlugin — Call another plugin’s method
Call a method already registered by the target plugin.- Permission:
network:fetch - Parameters:
(pluginId: string, method: string, ...args) - Rejects if the target is not installed / not active / has not registered the method
registerHook — Register a hook on a launcher method
Register the current plugin as an interceptor of a launcher method, injecting logic before and after it runs (modify arguments or the return value, block execution, or replace the implementation entirely).- Permission:
hook:register(dangerous) - Parameters:
(method: string, handler: (ctx, next) => Promise<void>) handleris Koa-style onion middleware: modifyctx.argsbeforenext(), modifyctx.resultafternext(), and usectx.prevent()to block the default implementation- All of the plugin’s hooks are unregistered automatically when the plugin is disabled
- See Hooks for details
callWasm — Call an exported WASM plugin function
Call a function exported by an L3 WASM plugin (on_load / on_unload / custom exports). It runs through the launcher’s Rust gateway (wasmtime).
- Permission:
wasm:execute - Parameters:
(pluginId: string, exportName?: string);exportNamedefaults toon_load - Returns:
{ ok: true, result: ... }or an error - See WASM plugins for details
listWasmPlugins — List loaded WASM plugins
- Permission:
wasm:execute
readText — Read a text file (authorization required)
Read a text file (UTF-8) at the given path, with support for partial reads. First access requires user authorization (a confirmation dialog).- Permission:
filesystem:read - Parameters:
(path: string, options?: { start?: number; length?: number }) start: start offset (characters), defaults to 0length: number of characters to read, defaults to the rest of the file- Returns:
{ path, content: string } - Authorization: the first access to an unauthorized path returns 403
FS_AUTHORIZATION_REQUIRED; the launcher automatically asks “allow this plugin to access this path?” and, once the user agrees, grants access (persisted per path prefix). The same plugin and path no longer prompts - Missing file →
FS_FILE_NOT_FOUND(404)
readBytes — Read a binary file (authorization required)
Read a binary file at the given path and return base64, with support for partial reads.- Permission:
filesystem:read - Parameters:
(path: string, options?: { start?: number; length?: number }) start: start offset (bytes), defaults to 0length: number of bytes to read, defaults to the rest of the file- Returns:
{ path, contentBase64: string } - Authorization works the same as
readText
writeText — Write a text file (authorization required)
- Permission:
filesystem:write - Parameters:
(path: string, content: string), written as UTF-8 - Authorization works the same as
readText; parent directories are created automatically - Returns:
{ path }
writeBytes — Write a binary file (authorization required)
- Permission:
filesystem:write - Parameters:
(path: string, bytes: Uint8Array), converted to base64 for transport automatically - Authorization works the same as
readText; parent directories are created automatically - Returns:
{ path }
deleteFile — Delete a file (authorization required)
- Permission:
filesystem:write - Parameters:
(path: string); deletes files only — usedeleteDirfor directories - Authorization works the same as
readText(destructive operations also require confirmation) - Missing file →
FS_FILE_NOT_FOUND(404) - Returns:
{ path }
deleteDir — Delete a directory (authorization required)
- Permission:
filesystem:write - Parameters:
(path: string, options?: { recursive?: boolean }) - Authorization works the same as
readText; the path must be inside a granted prefix - With
{ recursive: false }only an empty directory is accepted; otherwiseFS_DIR_NOT_EMPTY(400) - Dangerous roots are always refused: drive roots (
D:,D:\) and the filesystem root returnFS_REFUSE_ROOT(403). Grants are prefix-based, so a plugin granted a whole drive could otherwise wipe it in one call — plugins should still validate in their own domain (for example, refuse directories that are too shallow) - Missing directory →
FS_DIR_NOT_FOUND(404) - Returns:
{ path, removed }
Typical use: deleting an instance or server directory. Previously the backend could only
delete individual files, so a plugin had to recursively deleteFile and then rename the
empty shell away — leaving unremovable junk directories on disk.
execCommand — Run a shell command
Run a system command and return the exit code and output. Uses powershell on Windows and /bin/sh on Linux/macOS.- Permission:
shell:execute(dangerous permission, shown in red at install time) - Parameters:
(command: string, timeoutMs?: number);timeoutMsdefaults to 15000, range 1000–120000 - Returns:
{ exitCode: number, stdout: string, stderr: string } - Timeout →
SHELL_TIMEOUT(400); the command is terminated forcibly - Empty command →
SHELL_COMMAND_REQUIRED(400)
execCommand is a one-shot execution model: stdout/stderr are fully buffered until the process exits, and there is no process handle and no stdin. Do not use it to start resident processes (game servers, daemons, and so on) — they would be killed on timeout. Use process.* below for those scenarios.
process.spawn — Start a resident process
Start a long-running external process (such as a Minecraft serverjava -jar server.jar nogui) and get a handle back.
- Permission:
process:manage(dangerous permission) - Parameters:
{ command: string, args?: string[], cwd?: string, env?: Record<string, string> } - Returns:
{ pid: number, startedAtMs: number } - stdout/stderr are buffered line by line into a ring buffer (2000 lines), fetched incrementally via
process.logs - Ownership isolation: a plugin can only manage processes it spawned itself (pids of other plugins always return 404
PROCESS_NOT_FOUND) - Up to 32 concurrently managed processes per plugin (
PROCESS_SPAWN_FAILED) - After the process exits, the handle is kept for 10 minutes for final log reads, then reclaimed
process.stdin — Write to the process stdin
- Permission:
process:manage - Parameters:
(pid: number, text: string, opts?: { appendNewline?: boolean }) - This is how interactive console commands work (server
stop/say/whitelist addand so on) - Process already exited →
PROCESS_STDIN_FAILED
process.kill — Kill the process tree
- Permission:
process:manage - Kills the whole process tree (Windows
taskkill /T /F; Unix kills descendants one by one) - Idempotent: killing an already-exited process succeeds
process.alive — Query process state
- Permission:
process:manage - When
alive: false,exitCodeholds the exit code (nullif killed by a signal or unavailable)
process.logs — Cursor-based incremental logs
- Permission:
process:manage - Parameters:
(pid: number, afterSeq?: number); omittingafterSeqstarts from the oldest buffered line - Returns:
{ lines: [{ seq, stream, text }], nextSeq, oldestSeq, alive, exitCode } oldestSeq: the oldest seq still held after ring-buffer eviction — if your cursor lags beyond the buffer capacity (2000 lines), the middle lines are lost; surface this to the user- Logs remain fetchable after the process exits (within the retention window)
process.list — List this plugin’s managed processes
- Permission:
process:manage - Typical use: after a plugin page reload or launcher restart, recover still-running processes this way (together with a persisted pid)
pickFile — File picker dialog
Open a native file picker dialog (implemented in the main window via Tauri). In the sandbox,<input type="file"> cannot get absolute paths and window.showOpenFilePicker is unavailable — when you need an absolute path, this is the way.
- Permission:
dialog:open - Parameters:
{ title?, defaultPath?, filters?: [{ name, extensions: string[] }], multiple? }, all optional - Returns:
string | null(absolute path of the selection;nullon cancel)
pickDirectory — Directory picker dialog
- Permission:
dialog:open - Returns:
string | null
extractArchive — Extract a local zip to a directory (authorization required)
- Permission:
filesystem:write - Parameters:
(archivePath: string, destPath: string, opts?: { prefix?: string, stripPrefix?: boolean }); the destination directory is created automatically prefixextracts only entries under that sub-directory; withstripPrefix: truethe prefix is stripped — use these to lift a modpack’soverrides/to the server root (overrides/mods/a.jar→mods/a.jar)- Guards: zip-slip (illegal path entries rejected) plus entry-count / per-file / total-size limits (same extractor as modpacks)
- Authorization: the archive path needs read authorization and the destination needs write authorization — the first use shows one confirmation that authorizes both path prefixes
- zip only (rar/7z not supported yet)
listDir — List directory entries (authorization required)
- Permission:
filesystem:read - Parameters:
(path: string, opts?: { recursive?: boolean, maxEntries?: number }) maxEntriesdefaults to 1000, capped at 10000- Authorization works the same as
readText - Typical use: detect the server jar after an import, check whether
eula.txt/server.propertiesexist
sendNotification — System-level notification
Send a system-level desktop notification (WebView Notification API; the first call triggers the browser permission prompt). This reaches users even when the plugin page is not in the foreground. Returnsfalse on failure/denial — fall back to showToast.
- Permission:
system:notification - Parameters:
{ title: string, body?: string } - Returns:
boolean(truewhen the notification was shown;falsewhen the API is unavailable / not granted / denied)
openUrl — Open an external link
Open an external URL in the system default browser. Inside the L2 sandbox,window.open is blocked by the sandbox attribute, so use this method for external links.
- Permission:
system:notification - Uses
POST /api/system/open-urlunder the hood; onlyhttp:///https://are accepted; invalid URLs return 400
getSystemInfo — Read system and launcher information
Read OS, launcher, and memory information. Useful forminLauncherVersion compatibility checks or detecting the platform at runtime.
- Permission:
system:info - Uses
GET /api/systeminfounder the hood (equivalent to/api/system/info) GitCommitis the launcher’s Git commit hash;Memory/AvailableMemoryare byte countsDataDir: the launcher data directory (such as%LOCALAPPDATA%/qomicex-launcher). Use it to locate a default workspace for the plugin’s own data (servers, saves, downloads), e.g.{DataDir}/Servers- Launcher metadata fields:
LauncherName: launcher name (such asQomicex Launcher)LauncherVersion: launcher version (assembly version, such as0.1.1)VersionType: version type, derived from the prerelease tag of the version number (alpha/beta/rc/dev, otherwisestable)Developers: developer list as[{ name, role }]CoreDependencies: core library list as[{ name, version, license }](.NET / Tauri / Rust / Qomicex.Core and so on)
listPlugins — List installed plugins
Return the list of installed plugins, useful for dependency checks or store integration.- Permission:
plugin:list - Uses
GET /api/plugins/under the hood
uploadPlugin — Install a plugin
Upload a.qplugin package to install a plugin.
- Permission:
plugin:install(dangerous permission) - Parameters:
(fileData: number[], fileName: string); the multipart field name isplugin
navigate — Navigate to a page
Navigate to an internal launcher route.- Permission:
config:read - Note: use internal launcher routes, not external URLs
openPluginSettings — Open the plugin settings page
Navigate to “Settings → Plugins → Plugin settings” and focus the settings tab of the given plugin. Useful for a “settings” button on plugin pages and Overlay windows.- Permission:
config:read - Parameters:
(targetPluginId?: string); defaults to the current plugin - The target plugin must declare
contributes.settingsPagesin its manifest to have a tab (see manifest); if the target has no settings page, the plugin settings area still opens and stays at the default position - If a plugin has several settings pages, the first one is focused
showToast — In-app notification
Show a toast message.- Permission:
ui:toast - Parameters:
(message: string, type?: 'info' | 'error' | 'success'), defaults toinfo
log — Write plugin logs
Write a log line into the launcher log so it shows up in Settings → Logs.- Permission:
plugin:log - Parameters:
(message: string, level?: 'debug' | 'info' | 'warn' | 'error'), defaults toinfo
File drag-and-drop event (file-drop)
When files are dropped onto the launcher window, the main window emits afile-drop event whose payload is the array of absolute paths of the dropped files. The frontend (main UI) can listen with @tauri-apps/api/event:
- Emitted by the launcher’s Rust
WindowEvent::DragDrop::Drop { paths, .. }(whendragDropEnabledis on) - Paths are absolute system paths; well suited to interactions such as “drop a file to fill in its path”
- Note: the native
dropevent’sFileobject does not expose.pathin the frontend (Tauri intercepts system drag-and-drop); you must use this event - For L2 sandbox plugins: the main UI frontend listens and relays via
__pluginRegistry.call(pluginId, 'methodName', [paths])/callPlugin, and the plugin receives it withregisterMethod
getThemeColor — Get the current theme seed color
Return the seed color hex value of the current launcher theme so plugins can adapt their UI to it.- Permission:
config:read - Returns:
string | null(#rrggbbformat, ornull) - Logic: prefers the static hex color from appearance settings; in Monet dynamic mode (
background), takes the current--primaryCSS variable and converts it to hex
applyThemeOverride — Apply a theme override
Dynamically override the launcher UI’s CSS variables (color theme) to implement plugin-driven global theming.- Permission:
config:write - Parameters:
(vars: Record<string, string>)— keys are semantic tokens (dot or hyphen naming supported), values areH S% L%strings - Safety restrictions:
- Token allowlist: only 30 predefined semantic tokens such as
background,foreground,primary,accent, andborderare allowed - HSL format validation: values must strictly match
^\d{1,3} \d{1,3}% \d{1,3}%$, preventing arbitrary CSS injection - Implementation: injects
<style id="qomicex-plugin-theme-override">intodocument.head; the:root:rootselector guarantees high priority - Repeated calls replace the previous override styles
- The override is not cleared automatically when the plugin is disabled; call
clearThemeOverridemanually
clearThemeOverride — Clear the theme override
Remove the override styles injected byapplyThemeOverride and restore the original theme.
- Permission:
config:write - Removes the
<style>element withid="qomicex-plugin-theme-override" - Recommended in the plugin’s disable hook or when the user cancels the custom theme
overlay.create — Create an Overlay window
Create a draggable standalone Overlay window and return its id.- Permission:
ui:sub_window - Returns: the Overlay window id (string)
- See Overlay window development for details
overlay.show / hide / destroy — Overlay window control
- Permission:
ui:sub_window
overlay.setHtml — Update Overlay window content
- Permission:
ui:sub_window
overlay.setPosition — Move an Overlay window
- Permission:
ui:sub_window
addMenuItem — Register a sidebar menu item dynamically
Add a sidebar menu item at runtime without editing the manifest.- Permission:
ui:inject_sidebar - Parameters:
PluginMenuItem(the same element ascontributes.menuItems) - Dynamically registered menu items are removed automatically when the plugin is disabled
onLauncherExit — Launcher exit event
Register a callback that runs just before the launcher window closes (best effort). Suitable for fast synchronous cleanup: saving state, informing the user about processes the plugin spawned, and so on.- No permission required (passive event)
- The callback is synchronous and best-effort: the launcher does not wait for plugins and proceeds with the close flow — do not send network requests or start long async work inside the callback (it is not guaranteed to finish)
- Only fires during the normal close flow of the main launcher window; force-killed / crashed processes cannot guarantee it
getPluginStrings — Read the plugin’s own i18n bundle
Returns the merged string bundle of this plugin’sl10n/ directory (bundle.json as the base, overridden by bundle.<locale>.json for the current locale). The same source the manifest %key% resolution uses; render the plugin’s own UI with it.
- No permission required (reads the plugin’s own data)
- Requires
"l10n": "l10n"in the manifest (see manifest)
getLocale — Current launcher language
- No permission required; consistent with the overlay layer of
getPluginStrings
Icons
Recommended: <Glyph> + per-icon subpath (zero IPC, tree-shakeable)
Do not route in-page icons through the bridge. Icon data ships with @qomicex/plugin-ui;
static imports render synchronously:
fa-solid fa- prefix (fa-solid fa-folder-open →
@qomicex/plugin-ui/icons/folder-open). Brand icons use a brand- prefix
(fa-brands fa-github → @qomicex/plugin-ui/icons/brand-github). The export name is
PascalCase + Icon.
The bundler keeps only the icons you import (27 icons measured 0 redundant entries) and there
is no IPC round-trip at all.
<Icon name> — when the icon name comes from runtime data
name is a runtime string the bundler cannot analyse it, so the entire icon table
(~160KB) is lazily loaded as a separate chunk — only downloaded when an <Icon> actually
renders. Prefer <Glyph> whenever the name is known at compile time.
Unknown names render null (no empty gap) and console.warn in dev builds.
Discovering icons: listIcons
- No permission required
- Returns every usable key: built-in table + current colour-theme mapping + this plugin’s
contributes.iconTheme - Use it for icon pickers and self-checks rather than hardcoding a list (the built-in table evolves with the launcher)
Low level: getIconSvg
- No permission required
- Lookup order: this plugin’s
contributes.iconTheme→ current colour-theme mapping → the launcher’s built-in table; thesourcefield reports which one matched (plugin/theme/builtin) - A miss is no longer silent: the console warns with pointers to
listIcons()andcontributes.iconTheme - Only call it directly when the name comes from runtime data and
<Icon>is not a good fit
Icon names must exist in the launcher’s built-in table
A wrongfa-solid fa-* / fa-brands fa-* renders blank (historically silently, which let the
problem reach users). There are now two gates:
qomicex verify/qomicex lintscan source literals and the manifesticonfield, report an error for names outside the table, and suggest the closest built-in icons;<Icon>warns on a runtime miss.
contributes.iconTheme (see manifest — icon field spec),
or ship an .svg and render it with <img>.
Language change event (plugin:locale-changed)
Broadcast to all plugin sandboxes when the launcher language changes; re-fetchgetPluginStrings and re-render (aligned with theme following):
- No permission required (passive event)
Downloads and installation
download.addTask — Create a download task
Create a download task. The task appears in the launcher’s “Download center”, its progress is driven by SSE pushes, and it can be cancelled.- Permission:
download:manage - Returns:
{ taskId, status, targetPath } - The backend reuses
DownloadSessionManager(type"resource"), the same pipeline as mod downloads
download.progress — Query download progress
- Permission:
download:manage - Returns
nulliftaskIddoes not exist
download.list — List all download sessions
- Permission:
download:manage - Returns: an array of snapshots with the same shape as
download.progress
download.cancel — Cancel a download
- Permission:
download:manage
download.addBatch — Batch download (one card)
Download many files with a single call while occupying one card in the download center — downloading hundreds of mods won’t flood the list. Under the hood it shares the install pipeline’s downloader (concurrency, same-destination dedup, mirror fallback, atomic.part rename).
- Permission:
download:manage - Returns:
{ taskId, batchId, totalFiles } - The download runs in the background; poll
download.batchStatus(batchId)for completion - Mirror fallbacks apply automatically: Modrinth/CF files → MCIM, Forge/NeoForge maven → ForgeCDN/BMCLAPI, vanilla libraries → BMCLAPI — pass official URLs and the launcher picks the rest
download.batchStatus — Batch download status
- Permission:
download:manage - Poll every 800ms–1s; on
failed,errorcarries the first failure reason
download.registerInstall — Register an install task in the download center
Register only a “game install” task (type'game') in the frontend download center without creating a real download. Useful so users can see progress in the download center after a plugin starts an installation.
- Permission:
instance:write
Plugin-defined tasks (step progress)
Long pipelines orchestrated by the plugin itself (parse → download → install → configure) report progress viatask.*: tasks go into InstallTracker and show up in the download center automatically, rendered with the same step cards as built-in installs.
task.register — Register a step task
- Permission:
download:manage weightonly affects the aggregate progress ratio (normalized internally);labelis the step text on the card (plugin-supplied, no launcher i18n changes needed)- Throws
TASK_BUSY(409) when the sametaskIdalready has a non-terminal task
task.update — Advance steps / status
- Permission:
download:manage - All fields optional; only the provided ones are updated. Terminal statuses (completed/failed/cancelled) passed via
statusare rejected with 400 — finish tasks throughtask.complete
task.complete — Finish a task
- Permission:
download:manage - When the task carries
blockedFiles(restricted-file count), a successful finish becomesawaiting-filesinstead of completed (to remind the user to add files)
task.progress — Query task progress
- Permission:
download:manage
curseforge.files — CurseForge metadata proxy
Resolves CurseForgeprojectID:fileID pairs into direct download URLs. The API key never leaves the launcher process — this is the only CF lookup channel available to plugins.
- Permission:
network:fetch - Up to 20000 files per call (
CF_TOO_MANY_FILES)
Filesystem extensions
readArchiveEntry — Read a single zip entry (authorization required)
Read one entry from an archive without extracting the whole thing. Mod/pack metadata lives inside the archives (fabric.mod.json, META-INF/mods.toml, modrinth.index.json) — this is the fastest way to read it.
- Permission:
filesystem:read, plus per-path authorization for the archive (same flow asreadText, first use shows a confirmation) maxBytesdefaults to 4 MiB, capped at 32 MiB; exceeding it raisesARCHIVE_ENTRY_TOO_LARGE- zip containers only (jar/mrpack/litemod are all zip)
renamePath — Move / rename (authorization required)
- Permission:
filesystem:write; both the source and destination paths need authorization (one confirmation grants both) - Supports directories; the destination parent directory is created automatically
- Without “move”, a plugin would have to readBytes + writeBytes + deleteFile (triple-copying large files) — use this for things like quarantining client-only mods into another directory
modpack.install — One-click modpack install
Install a modpack directly through the launcher’s normal modpack installation flow (the same pipeline as the frontend “Modpacks” page): parse the source → create the instance → download and install.- Permission:
instance:write - Uses
POST /api/modpack/install-direct, reusingModpackService(ParseFileAsync/ResolveOnlineAsync/InstallAsync) andInstallTracker - Returns:
{ instanceId }; installation runs asynchronously (check progress in the download center / via SSE) - Error codes:
MODPACK_NAME_REQUIRED,MODPACK_GAME_DIR_REQUIRED,MODPACK_FILE_NOT_FOUND,MODPACK_SOURCE_REQUIRED,MODPACK_SOURCE_INVALID - To show install progress in the download center, register it with
download.registerInstall
Modpack-to-server (reference implementation)
“Turning a modpack into a runnable Minecraft server” has no dedicated endpoint — the launcher only provides generic capabilities, and the flow is orchestrated by the plugin using the APIs above. The official plugintop.qomicex.server-manager (src/api/setup.js) is a complete example. Pipeline and key points:
- Parse the pack:
readArchiveEntryreadsmodrinth.index.json(Modrinth) ormanifest.json(CurseForge) from the pack to get the MC version / loader / file list. For servers filter byenv.server != "unsupported"(different from the client’senv.client == "required"). - Install the runtime (
download.addBatch+process.spawn):- vanilla:
proxyFetchthe version manifest → downloadserver.jar(download.addBatchattaches the BMCLAPI fallback automatically) - fabric: fabric meta → download the server-launcher jar
- forge / neoforge: download the official installer → pre-fill
libraries/first (read the library list embedded in the installer viareadArchiveEntryfrominstall_profile.json/version.json, then download them concurrently throughdownload.addBatchso the NeoForge maven gets its ForgeCDN fallback) → thenprocess.spawnwithjava -jar installer --installServer, pollingprocess.alivefor the exit code. The installer skips files that already exist with a valid sha1; its--mirrorflag does not cover neoforged.net (verified empirically)
- vanilla:
- Download mods: Modrinth manifests carry direct URLs; CurseForge goes through
curseforge.filesfor direct links. Files whose link is unavailable (author disabled distribution) are recorded in{dir}/.qml-server/blocked-mods.json, with progress reported viatask.update - Release overrides:
extractArchive(packPath, dir, { prefix: 'overrides', stripPrefix: true }) - Quarantine client-only:
listDirscansmods/*.jar,readArchiveEntryreadsfabric.mod.json’senvironmentor(neo)forge.mods.toml’sclientSideOnly, and matches are moved withrenamePathinto{dir}/mods-client-only/(not deleted, recoverable). Note: mods whose metadata does not declare a side cannot be detected statically — they can only be quarantined manually after a startup crash - Write base config:
writeTextcreateseula.txt(false; the consent flow belongs to the user) /server.properties/user_jvm_args.txt; existing files are left untouched (overrides may carry the real config) - Write the launch manifest:
{dir}/.qml-server/launch.json(read viareadText, which requires authorization for that directory):
mode=argfile:java @<windowsArgfile or unixArgfile>(pick by platform);mode=jar:java -jar <jar>- Always run servers with
nogui: the GUI takes over console output and never reads the stdin pipe (the stop command stops working) - Progress is reported throughout via
task.register/task.update/task.complete, so the download center shows the step cards
Error handling
All API calls reject on failure with messages like:Permission denied: requires xxx— the manifest permissions do not include itBackend error: 404— thecallBackendendpoint does not existProxy failed: 400—proxyFetchparameter error (including SSRF blocking)Plugin xxx does not provide method yyy (it may not be installed or activated)— thecallPlugintarget is unavailable