Skip to main content

Plugin API reference

Plugin scripts interact with the launcher through the global object window.__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 injects window.__PLUGIN_API_BASE__ with the file access base URL of the current plugin:
Use it to build URLs for bundled resources such as icons and images:
The variable does not exist in Inline rendering (inline rendering)(no 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’s permissions 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 own settings.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; without data → GET
  • You can pass a _method field in the data object to override the HTTP method (for example { _method: 'PUT', State: 'active' }); _method is not sent to the backend
  • endpoint starts with / (such as /instance) and excludes the /api prefix
  • 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
In the L2 sandbox (sandbox="allow-scripts", no allow-popups), window.open(url, '_blank') is blocked. To open external links, use the backend endpoint:
This endpoint opens 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: true when forwarding the upstream stream
  • signal (AbortSignal) aborts the stream; req.signal is stripped automatically before postMessage and sent as a __plugin_api_abort message instead
  • ⚠️ Known limitation: the Promise returned by proxyFetchStream resolves when the stream ends, but chunk callbacks may still fire after it resolves. Use onError or 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 via callPlugin.
  • Permission: config:write
  • Parameters: (method: string, fn: Function); fn may 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>)
  • handler is Koa-style onion middleware: modify ctx.args before next(), modify ctx.result after next(), and use ctx.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); exportName defaults to on_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 0
  • length: 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 0
  • length: 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 — use deleteDir for 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; otherwise FS_DIR_NOT_EMPTY(400)
  • Dangerous roots are always refused: drive roots (D:, D:\) and the filesystem root return FS_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); timeoutMs defaults 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 server java -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 add and 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, exitCode holds the exit code (null if killed by a signal or unavailable)

process.logs — Cursor-based incremental logs

  • Permission: process:manage
  • Parameters: (pid: number, afterSeq?: number); omitting afterSeq starts 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; null on 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
  • prefix extracts only entries under that sub-directory; with stripPrefix: true the prefix is stripped — use these to lift a modpack’s overrides/ 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 })
  • maxEntries defaults 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.properties exist

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. Returns false on failure/denial — fall back to showToast.
  • Permission: system:notification
  • Parameters: { title: string, body?: string }
  • Returns: boolean (true when the notification was shown; false when the API is unavailable / not granted / denied)
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-url under the hood; only http:// / https:// are accepted; invalid URLs return 400

getSystemInfo — Read system and launcher information

Read OS, launcher, and memory information. Useful for minLauncherVersion compatibility checks or detecting the platform at runtime.
  • Permission: system:info
  • Uses GET /api/systeminfo under the hood (equivalent to /api/system/info)
  • GitCommit is the launcher’s Git commit hash; Memory / AvailableMemory are byte counts
  • DataDir: 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 as Qomicex Launcher)
  • LauncherVersion: launcher version (assembly version, such as 0.1.1)
  • VersionType: version type, derived from the prerelease tag of the version number (alpha / beta / rc / dev, otherwise stable)
  • 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 is plugin
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.settingsPages in 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 to info

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 to info

File drag-and-drop event (file-drop)

When files are dropped onto the launcher window, the main window emits a file-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, .. } (when dragDropEnabled is on)
  • Paths are absolute system paths; well suited to interactions such as “drop a file to fill in its path”
  • Note: the native drop event’s File object does not expose .path in 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 with registerMethod

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 (#rrggbb format, or null)
  • Logic: prefers the static hex color from appearance settings; in Monet dynamic mode (background), takes the current --primary CSS 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 are H S% L% strings
  • Safety restrictions:
  • Token allowlist: only 30 predefined semantic tokens such as background, foreground, primary, accent, and border are 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"> into document.head; the :root:root selector guarantees high priority
  • Repeated calls replace the previous override styles
  • The override is not cleared automatically when the plugin is disabled; call clearThemeOverride manually

clearThemeOverride — Clear the theme override

Remove the override styles injected by applyThemeOverride and restore the original theme.
  • Permission: config:write
  • Removes the <style> element with id="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.

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 as contributes.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’s l10n/ 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

Do not route in-page icons through the bridge. Icon data ships with @qomicex/plugin-ui; static imports render synchronously:
The subpath is the class name without the 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

Because 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; the source field reports which one matched (plugin / theme / builtin)
  • A miss is no longer silent: the console warns with pointers to listIcons() and contributes.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 wrong fa-solid fa-* / fa-brands fa-* renders blank (historically silently, which let the problem reach users). There are now two gates:
  1. qomicex verify / qomicex lint scan source literals and the manifest icon field, report an error for names outside the table, and suggest the closest built-in icons;
  2. <Icon> warns on a runtime miss.
To use an icon the built-in table lacks, declare a private icon via 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-fetch getPluginStrings 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 null if taskId does 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, error carries 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 via task.*: 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
  • weight only affects the aggregate progress ratio (normalized internally); label is the step text on the card (plugin-supplied, no launcher i18n changes needed)
  • Throws TASK_BUSY (409) when the same taskId already 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 status are rejected with 400 — finish tasks through task.complete

task.complete — Finish a task

  • Permission: download:manage
  • When the task carries blockedFiles (restricted-file count), a successful finish becomes awaiting-files instead of completed (to remind the user to add files)

task.progress — Query task progress

  • Permission: download:manage

curseforge.files — CurseForge metadata proxy

Resolves CurseForge projectID: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 as readText, first use shows a confirmation)
  • maxBytes defaults to 4 MiB, capped at 32 MiB; exceeding it raises ARCHIVE_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, reusing ModpackService (ParseFileAsync/ResolveOnlineAsync/InstallAsync) and InstallTracker
  • 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 plugin top.qomicex.server-manager (src/api/setup.js) is a complete example. Pipeline and key points:
  1. Parse the pack: readArchiveEntry reads modrinth.index.json (Modrinth) or manifest.json (CurseForge) from the pack to get the MC version / loader / file list. For servers filter by env.server != "unsupported" (different from the client’s env.client == "required").
  2. Install the runtime (download.addBatch + process.spawn):
    • vanilla: proxyFetch the version manifest → download server.jar (download.addBatch attaches 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 via readArchiveEntry from install_profile.json / version.json, then download them concurrently through download.addBatch so the NeoForge maven gets its ForgeCDN fallback) → then process.spawn with java -jar installer --installServer, polling process.alive for the exit code. The installer skips files that already exist with a valid sha1; its --mirror flag does not cover neoforged.net (verified empirically)
  3. Download mods: Modrinth manifests carry direct URLs; CurseForge goes through curseforge.files for direct links. Files whose link is unavailable (author disabled distribution) are recorded in {dir}/.qml-server/blocked-mods.json, with progress reported via task.update
  4. Release overrides: extractArchive(packPath, dir, { prefix: 'overrides', stripPrefix: true })
  5. Quarantine client-only: listDir scans mods/*.jar, readArchiveEntry reads fabric.mod.json’s environment or (neo)forge.mods.toml’s clientSideOnly, and matches are moved with renamePath into {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
  6. Write base config: writeText creates eula.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)
  7. Write the launch manifest: {dir}/.qml-server/launch.json (read via readText, 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:
Common errors:
  • Permission denied: requires xxx — the manifest permissions do not include it
  • Backend error: 404 — the callBackend endpoint does not exist
  • Proxy failed: 400 — proxyFetch parameter error (including SSRF blocking)
  • Plugin xxx does not provide method yyy (it may not be installed or activated) — the callPlugin target is unavailable