Integration guide¶
There are three supported integration surfaces. All use the same engine and the same result/error semantics.
- Go library — preferred for Go applications. Two calls, no daemon, no process boundary.
- Subprocess + NDJSON — universal fallback for other languages, process isolation, or embedding in non-Go build systems.
- Loopback HTTP API — for desktop apps that want a long-lived updater process, job polling, cancellation and SSE.
Go library¶
Import the module and keep the integration to New, Check and Apply:
import wmlupdate "github.com/yxxbc/wmlUpdate"
u, err := wmlupdate.New(wmlupdate.Options{
ConfigPath: "updater.json",
CurrentVersion: appVersion,
})
if err != nil { return err }
defer u.Close()
update, err := u.Check(ctx)
if err != nil { return err }
if !update.Available { return nil }
result, err := u.Apply(ctx, update, wmlupdate.ApplyOptions{})
Options.Config can replace ConfigPath for a dependency-only setup. Events are
an optional callback. The library never exits or restarts the host. See the
Go SDK guide for the complete API, Windows deferred apply, programmatic
configuration and error codes.
Subprocess + NDJSON¶
Run the binary directly. Do not invoke it through a shell: paths, arguments and exit codes are safer when passed as an argv array.
When --json is set:
- stdout contains only JSON objects, one per line.
- stderr contains human diagnostics, if any.
- Never parse prose from stdout.
- Read stdout line by line as the process runs; do not wait for exit before displaying progress.
- Treat the terminal
resultorerrorevent as authoritative, and also check the process exit code.
Event shapes¶
Progress:
{"type":"progress","time":"2026-10-28T12:00:00Z","phase":"download","bytes":1048576,"total":4194304,"percent":25}
Log and warning:
{"type":"log","time":"2026-10-28T12:00:00Z","phase":"source","message":"querying static source"}
{"type":"warning","time":"2026-10-28T12:00:00Z","message":"artifact has no SHA-256; security.allow_unverified_assets is enabled"}
Success:
{
"type": "result",
"time": "2026-10-28T12:00:01Z",
"data": {
"status": "updated",
"current_version": "1.5.0",
"previous_version": "1.4.2",
"latest_version": "1.5.0",
"channel": "stable",
"source": "static",
"tag": "1.5.0",
"published_at": "2026-10-27T09:00:00Z",
"notes": "Release notes",
"asset": {
"name": "myapp-1.5.0-linux-amd64.tar.gz",
"os": "linux",
"arch": "amd64",
"format": "tar.gz",
"sha256": "0123...",
"size": 4194304
},
"artifact_path": "/home/user/.local/state/wml-update/com.example.myapp/downloads/0123...__myapp-1.5.0-linux-amd64.tar.gz"
}
}
Failure:
{"type":"error","time":"2026-10-28T12:00:01Z","error":{"code":"integrity","message":"checksum mismatch: got ..., want ..."}}
Status values¶
| Status | Command | Meaning |
|---|---|---|
update_available |
check, update --dry-run |
A newer (or forced) release is available |
up_to_date |
check, update, fetch |
Current version is not older than the release |
updated |
update |
Installation completed |
downloaded |
update with install.type=external, fetch |
Verified artifact is available at artifact_path |
apply_scheduled |
update --defer |
Staged; detached helper will apply it |
rolled_back |
rollback |
Previous backup restored |
marked |
mark |
External installation recorded |
check exits 0 even when an update is available. Branch on result.data.status.
Error codes and exit codes¶
| Code | Exit | Meaning |
|---|---|---|
config |
2 | Invalid config, version or flags |
source / network |
3 | Source unavailable, malformed response, HTTP failure |
no_release / no_asset |
3 | Nothing eligible / no asset for this platform |
integrity |
4 | Signature, checksum or size failure |
install / rollback |
5 | Filesystem/install failure |
locked / state / io |
6 | Concurrent update, state or I/O failure |
cancelled |
130 | Context/signal cancellation |
internal |
1 | Unexpected error |
Choosing a command¶
checkis cheap and read-only. Use it on startup or a timer.updateis the normal path: check + download + verify + install.fetch --output PATHverifies an artifact and copies it to a path so your code can install it. Follow withmark --version ... --sha256 ...only after your installer succeeds.update --deferis required when the target is running on Windows.
Deferred apply (Windows and self-replacement)¶
app process ──spawn──> wml-update update --defer --wait-pid <appPID> --json
│ │
│ ├─ download, verify, extract, write plan
│ ├─ copy updater to state dir/helper/<plan>/
│ ├─ start helper detached
│ └─ emit result apply_scheduled; exit 0
│
└─ exits (after receiving apply_scheduled)
helper ── waits for <appPID> ── acquires lock ── journaled apply ── relaunch
Recommended sequence:
- Spawn the updater with
update --defer --wait-pid <yourPID> --json. - Read until the
resultevent. - If
status == "apply_scheduled", exit the application promptly. - Configure
install.relaunch_pathso the helper restarts the new version.
The parent updater must be allowed to finish emitting apply_scheduled before
the application exits. The helper is copied outside the managed directory, so it
can replace files even when the updater binary itself lives inside the target.
A helper result file is written at:
with {"ok":true} or an error. On failure the plan is retained for diagnosis;
the next update recovers any pending journal.
Language examples¶
All examples follow the same rules: argv array, line-delimited JSON, wait for a terminal event, check the exit code.
Loopback HTTP API¶
Start the server:
Example ready line (a token is generated when none is supplied):
{"type":"ready","url":"http://127.0.0.1:52143","address":"127.0.0.1:52143","pid":4242,"token_source":"generated","token":"..."}
If a token is supplied with --token, --token-env or --token-file, it is not
echoed; the caller already knows it. WML_UPDATE_TOKEN is read automatically if
no flag is set.
The server binds loopback by default. --allow-remote permits another address;
terminate TLS and additional authentication at a reverse proxy.
Authentication¶
Every endpoint except /v1/health requires one of:
Tokens must be at least 16 characters. Comparison is constant-time. There is no CORS opt-in.
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/health |
Liveness; no auth |
| GET | /v1/info |
App ID, channel, platform, source type |
| GET | /v1/status |
Persisted state |
| POST | /v1/check |
Synchronous check |
| POST | /v1/update |
Start asynchronous update job |
| POST | /v1/fetch |
Start asynchronous verified-download job |
| POST | /v1/rollback |
Start asynchronous rollback job |
| POST | /v1/mark |
Record an externally installed version |
| GET | /v1/jobs |
List jobs |
| GET | /v1/jobs/{id} |
Job snapshot with events |
| POST | /v1/jobs/{id}/cancel |
Cancel a running job |
| GET | /v1/jobs/{id}/events |
SSE event stream |
Only one mutating job (update, fetch, rollback) can run at a time. A second
returns HTTP 409 with code job_conflict.
Check¶
Returns the same result object used by the CLI (status, versions, notes,
asset). HTTP status reflects whether the check itself succeeded; up_to_date is
a 200, not an error.
Update job¶
POST /v1/update
Authorization: Bearer ...
{
"current_version": "1.4.2",
"force": false,
"defer": false,
"wait_pids": []
}
Returns HTTP 202:
Poll GET /v1/jobs/{id}:
{
"id": "9d3f...",
"kind": "update",
"status": "succeeded",
"result": {"status":"updated","previous_version":"1.4.2","current_version":"1.5.0"},
"events": [
{"type":"progress","phase":"download","bytes":100,"total":100,"percent":100},
{"type":"log","phase":"install","message":"artifact staged"}
]
}
status is running, succeeded, failed or cancelled. Errors use
error.code and error.message with the same stable codes as the CLI.
SSE events¶
Each event is an SSE data: frame containing the same JSON event object. The
job emits a terminal result or error event, then the stream closes after all
buffered events are delivered. The server polls internally every 250 ms.
Reconnect with Last-Event-ID to resume by index.
Minimal Python HTTP client¶
import json, urllib.request
def call(base, token, path, payload=None):
req = urllib.request.Request(
base + path,
data=json.dumps(payload or {}).encode(),
headers={"Authorization": "Bearer " + token, "Content-Type": "application/json"},
method="POST" if payload is not None else "GET",
)
with urllib.request.urlopen(req) as r:
return json.load(r)
Concurrency and lifetime¶
- The CLI is safe to call concurrently; the per-app file lock serializes writes.
- The HTTP API keeps jobs in memory and loses them on restart. Call
/v1/statusfor durable state. - Cancel is cooperative: it cancels the context, which stops downloads and source requests. A filesystem apply already in progress is short and may finish; rollback remains available.