Lance is a command-line tool for seamless multi-monitor remote desktop using Apollo (a Sunshine fork) and Moonlight. It manages the complexity of running one Apollo instance per monitor in parallel, so a single command opens or closes a full multi-monitor session.
Status: Alpha — fully functional for personal use. No service installer yet; both binaries are run manually.
Two components cooperate:
| Binary | Role | Runs on |
|---|---|---|
lance-agent |
Web API that manages Apollo instances | Remote machine (Windows) |
lance |
CLI that drives the session from the local machine | Local machine (Windows or Linux) |
lance connect asks the agent to start one Apollo instance per monitor, then
launches one Moonlight window per instance. Each instance uses an independent
configuration cloned from your existing Apollo setup. Each clone has its own
identity and must be paired with Moonlight once before it can be used.
A slot is Lance's term for one Apollo instance and its configuration. Slot 0
is your original Apollo config (the template); slots 1, 2, … are clones. lance status shows all slots and which ones have an active Moonlight connection.
Remote machine (agent)
- Windows, run as Administrator
- Apollo installed and paired with Moonlight at least once
- The Apollo service (
sunshinesvc.exe) stopped before running the agent — Lance manages its own Apollo processes directly and the two will conflict
Local machine (client)
- Moonlight installed (
moonlight.exeon Windows,moonlighton Linux) - Network access to the remote machine on the agent port (default: 9876)
Build machine
- .NET 10 SDK
- MSVC toolchain — required for AOT compilation (Windows only, only needed when building)
Run from the repository root on a Windows machine:
make publish
Optional:
make publish-keep-iis # keeps web.config and static web asset files in the agent output (rarely needed)
make test # run all tests
make ships with Git for Windows (Git Bash). If you prefer to invoke the script directly:
dotnet run scripts/publish.cs [--keep-iis-artifacts]
Outputs:
| Path | Contents |
|---|---|
dist/agent/ |
Agent binary + sample config — deploy to the remote machine |
dist/client/ |
Client binary + sample config — deploy to the local machine |
dist/client-linux/ |
Linux client binary — produced when the script runs on Linux |
- Copy
dist/agent/to a folder on the remote machine, e.g.C:\Lance\agent\. - Edit
lance-agent.json:- Set
remoteServer.installDirandremoteServer.configDirto your Apollo installation paths. - Set
auth.tokento a secret string to protect the API (recommended). Set it to""to run the API open with no authentication. tls.certPathis unused in the current release — HTTPS uses the ASP.NET Core developer certificate. Rundotnet dev-certs https --trustonce on the agent machine if you have not already done so.
- Set
- Stop the Apollo service if it is running.
- Run as Administrator:
lance-agent.exe
Logs are written to the console and to lance-agent.log (rolling daily). On
first run without a config file, built-in defaults apply and a warning is logged.
- Place
lance.exe(orlanceon Linux) somewhere on your PATH. - Copy
lance.jsonfromdist/client/beside the binary, or point to it with--config <path>. - Edit
lance.json:- Set
agent.urltohttps://<remote-machine-ip>:9876. - Set
agent.tokento matchauth.tokenfromlance-agent.json. - Adjust
remoteClient.executableif Moonlight is not on PATH. - Tune
remoteClient.defaultFlagsfor your setup (fps, codec, bitrate).
- Set
# Check slot states and active Moonlight connections
lance status
# List physical monitors on this machine (use the IDs with --monitors)
lance monitors
# Connect to all physical monitors
lance connect
# Connect to specific monitors only (1-indexed, comma-separated)
lance connect --monitors 1,3
# Connect with custom Moonlight flags (appended after the defaults; later flags win)
lance connect --monitors 1,2 --options "--fps 120 --bitrate 100000"
# Disconnect all active sessions (kills Moonlight, stops Apollo on the remote)
lance disconnect
# Disconnect specific slots only
lance disconnect --slots 1,2
# Disconnect but leave Apollo running on the remote (for quick reconnect)
lance disconnect --keep-running
# Stop Apollo and remove slot configs on the remote
lance disconnect --purge
# Low-level slot management
# <ids> is one id or a comma-separated list (e.g. 1 or 1,2,3); each id is
# processed independently (partial success — a failed id is logged, the rest proceed)
lance slots # list all slots
lance allocate <count> # ensure the pool has exactly <count> slots
lance start <ids> # start each slot's Apollo instance
lance stop <ids> # stop each slot's Apollo instance
lance deallocate <ids> # remove slot configs (slots must be stopped)
lance deallocate <ids> --force # stop if running, then remove config
# Open Apollo's web config page for one or more slots in the browser
lance config <ids>
Global options (work with any command):
-a, --agent <url> Override the agent URL for this invocation
-k, --token <value> Bearer token for the agent API
-c, --config <path> Use a specific lance.json
-v, --verbose Enable debug logging to stderr
--no-color Disable ANSI colour output
Place beside lance-agent.exe. Missing file → built-in defaults apply.
{
"listen": {
"host": "0.0.0.0",
"port": 9876
},
"tls": {
"certPath": "lance-agent.pfx"
},
"auth": {
"token": "ODlyrexDUv5jckPb7nUWBK9O"
},
"remoteServer": {
"installDir": "C:\\Program Files\\Apollo",
"configDir": "C:\\Program Files\\Apollo\\config",
"executable": "sunshine.exe",
"templateConfigName": "sunshine.conf",
"startupTimeoutSeconds": 30
},
"slots": {
"maxCount": 8,
"portStep": 1000,
"stopTimeoutSeconds": 10,
"namePrefix": "Lance",
"templateName": "Lance-Template",
"configNamePattern": "sunshine_{id}.conf"
},
"logging": {
"level": "Information",
"filePath": "lance-agent.log",
"retainDays": 7
}
}Place beside lance.exe / lance, or specify with --config <path>.
{
"agent": {
"url": "https://<agent-host>:9876",
"token": "ODlyrexDUv5jckPb7nUWBK9O",
"timeoutSeconds": 30
},
"remoteClient": {
"executable": "C:\\Program Files\\Moonlight Game Streaming\\moonlight.exe",
"defaultFlags": ["--fps", "60", "--video-codec", "HEVC", "--bitrate", "80000", "--no-vsync"]
},
"ui": { "color": true },
"logging": { "level": "Information", "filePath": null }
}Full sample files are in samples/.
Config file lookup (first match wins):
-c/--config <path>CLI flaglance.jsonbeside thelancebinary- Exit 7 if neither yields a URL
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic error |
| 2 | No free slots — all slots are connected; disconnect first |
| 3 | Agent unreachable |
| 4 | Agent returned an error |
| 5 | Moonlight launch failed |
| 6 | Slot not in required state |
| 7 | Agent URL could not be resolved |
- Pairing slots: each clone slot has its own Apollo identity and must be
paired with Moonlight individually before first use. Start the slot
(
lance start <id>), open Moonlight and add the host on that slot's port (host:<port>), complete the PIN pairing, then stop the slot (lance stop <id>). Only needs doing once per slot. - Monitor placement: on Windows, Lance automatically moves each Moonlight window to its target monitor after launch. On Linux, windows open on the primary monitor; manual placement is needed until Phase 3 adds Linux support.
- TLS: The agent always uses HTTPS with a self-signed certificate. The client skips certificate validation automatically (configurable cert pinning is planned for a later release).
- Partial success:
connectanddisconnectare best-effort per monitor — a failed monitor is logged and skipped; the others proceed. - Apollo service: Lance manages only the Apollo instances it launches directly.
The installed Apollo service (
sunshinesvc.exe) must be stopped before running the agent, otherwise the two will conflict for the same ports and config files.
| File | Contents |
|---|---|
| docs/ARCHITECTURE.md | System design, flows, and behavioral invariants |
| docs/SPEC.md | API contract, config shapes, ports, and mutation rules |