feat(sidecar): make the sidecar installable — CLI, --print-config, anchored data paths #24

Merged
whitlocktech merged 1 commits from feat/installable-cli into main 2026-08-04 15:48:12 +00:00
Member

Phase 0.2 of the installer plan (docs/installer/PLAN.md §5, Phase 0 item 2): "make the sidecar installable — confirm/settle default data paths, and add a way to read back config non-interactively so the installer does not have to scrape logs for the token."

Companion docs PR: RunicGateway/docs#84.

Why

The installer has to drive this binary non-interactively, and today it cannot:

  • The auth token — the one value the whole integration hinges on (PLAN.md §2.4 calls hunting for it the largest "I installed it and nothing happened" failure mode) — is only readable by scraping the startup log or parsing TOML.
  • The config path can only be named via $UOLINK_CONFIG, which is awkward for a one-off diagnostic run.
  • A relative db path follows the process working directory, which a service manager picks, not the operator (PLAN.md §2.3, the "working-directory trap").

What changed

New CLI (cli.rs) — four flags, hand-rolled; an argument-parsing dependency would be larger than the code it replaced.

uo-link-sidecar [--print-config] [--config <PATH>] [-V|--version] [-h|--help]

An unrecognized argument exits 2 rather than silently starting a sidecar that isn't the one asked for.

--print-config resolves the configuration exactly as a normal start does — including writing a missing config file and generating a blank auth token — and prints it as JSON on stdout:

{
  "component": "uo-link-sidecar",
  "config_created": false,
  "config_path": "/etc/runicgateway/sidecar.toml",
  "protocol": 3,
  "shard": { "bind": "127.0.0.1:7788" },
  "store": { "path": "/var/lib/runicgateway/uo-link.db" },
  "token_generated": false,
  "version": "0.1.0",
  "web": {
    "auth_required": true,
    "auth_token": "c0f04ace…",
    "bind": "127.0.0.1:8080",
    "ws_path": "/ws"
  }
}
  • config_created / token_generated let a re-run distinguish read an existing install from provisioned a new one — the values alone can't say.
  • The log subscriber is deliberately not started in this mode (it writes to stdout), so the document is the entire output.
  • ws_path comes from the constant the route is registered with, so the installer's WS URL can't drift from the server's.
  • The output contains the token in clear text. That is the point, and it is documented as a secret: don't pipe it to a log or a CI artifact.

Data paths settled:

  • A relative [store].path anchors to the config file's directory, not the CWD. A unit pinning UOLINK_CONFIG=/etc/runicgateway/sidecar.toml keeps its database beside its config instead of in %SystemRoot%\System32 or a silently redirected VirtualStore copy. Development is unaffected — under cargo run the two directories are the same. :memory: and file: URIs are left alone.
  • The db path is handed to sqlx as a filesystem path rather than formatted into a sqlite:// URL. That spelling is parsed as a URL: it percent-decodes the path and splits it on ?, so an installed path containing %20 opened a different file. Verified fixed on a directory literally named a%20b.
  • Parent directories are created for both the config and the database, so a service can name /var/lib/runicgateway/uo-link.db on a host where nothing has made that directory yet.
  • Reported paths are the resolved absolute ones, and the startup log now names the config and db it actually used.

No platform data directories are compiled in, deliberately: the installer owns layout and pins UOLINK_CONFIG / UOLINK_DB_PATH in the service definition (PLAN.md §2.3). The anchoring above is what makes a partially configured service (config pinned, db not) land somewhere sane instead of somewhere arbitrary.

No protocol changePROTOCOL_VERSION stays 3.

Verification

All three CI gates pass locally (cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo test --locked).

22 unit tests, up from 0 — argument parsing (including --config with no value and a typo'd flag), path anchoring, persist_token's three branches, the generated config template round-tripping through the TOML parser, and the --print-config document.

Smoke-tested against the real binary:

  • --version, --help, and an unknown flag exiting 2.
  • --print-config into a non-existent directory: created the tree, wrote the config, generated a token, reported config_created: true / token_generated: true. A second run reported both false and the same token.
  • Started from C:\Windows with UOLINK_CONFIG and UOLINK_DB_PATH pinned to a scratch tree: the db landed exactly where told, including on a directory named data 50%.
  • A directory named a%20b received the db literally — the case the URL spelling would have redirected to a b.

AI-assisted contribution

  • This PR was written with AI assistance (Claude Code / Claude Opus 5); commits carry the Co-Authored-By trailer.
Phase 0.2 of the installer plan ([`docs/installer/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/PLAN.md) §5, Phase 0 item 2): *"make the sidecar installable — confirm/settle default data paths, and add a way to read back config non-interactively so the installer does not have to scrape logs for the token."* Companion docs PR: RunicGateway/docs#84. ## Why The installer has to drive this binary non-interactively, and today it cannot: - The **auth token** — the one value the whole integration hinges on (`PLAN.md` §2.4 calls hunting for it the largest "I installed it and nothing happened" failure mode) — is only readable by scraping the startup log or parsing TOML. - The **config path** can only be named via `$UOLINK_CONFIG`, which is awkward for a one-off diagnostic run. - A relative **db path follows the process working directory**, which a service manager picks, not the operator (`PLAN.md` §2.3, the "working-directory trap"). ## What changed **New CLI (`cli.rs`)** — four flags, hand-rolled; an argument-parsing dependency would be larger than the code it replaced. ``` uo-link-sidecar [--print-config] [--config <PATH>] [-V|--version] [-h|--help] ``` An unrecognized argument exits `2` rather than silently starting a sidecar that isn't the one asked for. **`--print-config`** resolves the configuration exactly as a normal start does — including writing a missing config file and generating a blank auth token — and prints it as JSON on stdout: ```json { "component": "uo-link-sidecar", "config_created": false, "config_path": "/etc/runicgateway/sidecar.toml", "protocol": 3, "shard": { "bind": "127.0.0.1:7788" }, "store": { "path": "/var/lib/runicgateway/uo-link.db" }, "token_generated": false, "version": "0.1.0", "web": { "auth_required": true, "auth_token": "c0f04ace…", "bind": "127.0.0.1:8080", "ws_path": "/ws" } } ``` - `config_created` / `token_generated` let a re-run distinguish *read an existing install* from *provisioned a new one* — the values alone can't say. - The log subscriber is deliberately **not** started in this mode (it writes to stdout), so the document is the entire output. - `ws_path` comes from the constant the route is registered with, so the installer's WS URL can't drift from the server's. - The output **contains the token in clear text**. That is the point, and it is documented as a secret: don't pipe it to a log or a CI artifact. **Data paths settled:** - A relative `[store].path` anchors to the **config file's directory**, not the CWD. A unit pinning `UOLINK_CONFIG=/etc/runicgateway/sidecar.toml` keeps its database beside its config instead of in `%SystemRoot%\System32` or a silently redirected VirtualStore copy. Development is unaffected — under `cargo run` the two directories are the same. `:memory:` and `file:` URIs are left alone. - The db path is handed to sqlx as a **filesystem path** rather than formatted into a `sqlite://` URL. That spelling is parsed as a URL: it percent-decodes the path and splits it on `?`, so an installed path containing `%20` opened a *different* file. Verified fixed on a directory literally named `a%20b`. - Parent directories are created for both the config and the database, so a service can name `/var/lib/runicgateway/uo-link.db` on a host where nothing has made that directory yet. - Reported paths are the resolved absolute ones, and the startup log now names the config and db it actually used. **No platform data directories are compiled in**, deliberately: the installer owns layout and pins `UOLINK_CONFIG` / `UOLINK_DB_PATH` in the service definition (`PLAN.md` §2.3). The anchoring above is what makes a *partially* configured service (config pinned, db not) land somewhere sane instead of somewhere arbitrary. **No protocol change** — `PROTOCOL_VERSION` stays `3`. ## Verification All three CI gates pass locally (`cargo fmt --check`, `cargo clippy --locked --all-targets -- -D warnings`, `cargo test --locked`). 22 unit tests, up from 0 — argument parsing (including `--config` with no value and a typo'd flag), path anchoring, `persist_token`'s three branches, the generated config template round-tripping through the TOML parser, and the `--print-config` document. Smoke-tested against the real binary: - `--version`, `--help`, and an unknown flag exiting `2`. - `--print-config` into a **non-existent** directory: created the tree, wrote the config, generated a token, reported `config_created: true` / `token_generated: true`. A second run reported both `false` and the same token. - Started from `C:\Windows` with `UOLINK_CONFIG` and `UOLINK_DB_PATH` pinned to a scratch tree: the db landed exactly where told, including on a directory named `data 50%`. - A directory named `a%20b` received the db literally — the case the URL spelling would have redirected to `a b`. ## AI-assisted contribution - [x] This PR was written with AI assistance (Claude Code / Claude Opus 5); commits carry the `Co-Authored-By` trailer.
wtclaude added 1 commit 2026-08-04 15:45:54 +00:00
feat(sidecar): make the sidecar installable — CLI, --print-config, anchored data paths
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 7m52s
81becaf7c8
Phase 0.2 of the installer plan (docs/installer/PLAN.md §5). The installer has to
drive this binary non-interactively, and today it cannot: the auth token is only
readable by scraping the startup log, the config path can only be named through an
environment variable, and a relative db path follows the process working directory —
which a service manager, not the operator, chooses.

- Add a four-flag CLI (cli.rs): --print-config, --config <PATH>, --version, --help.
  Hand-rolled; an argument-parsing dependency would be larger than the code it
  replaced. An unrecognized flag exits 2 rather than starting a sidecar that is not
  the one that was asked for.
- --print-config resolves the configuration exactly as a normal start does —
  including writing a missing config file and generating a blank auth token — and
  prints it as JSON on stdout: versions, protocol, both bind addresses, ws path,
  resolved db path, and the token. config_created / token_generated let a re-run
  tell "read an existing install" from "provisioned a new one". The log subscriber
  is deliberately not started in this mode, so the document is the whole output.
- Anchor a relative [store].path to the config file's directory instead of the CWD,
  and report resolved absolute paths. A unit pinning UOLINK_CONFIG now keeps its
  database beside its config rather than in %SystemRoot%\System32 or a VirtualStore
  redirect. Development is unaffected: under cargo run the two directories are the
  same. :memory: and file: URIs are left alone.
- Hand the db path to sqlx as a filesystem path instead of formatting it into a
  sqlite:// URL, which percent-decodes it and splits it on '?'. An installed path
  containing %20 previously opened a different file; verified it now does not.
- Create the config's and the database's parent directories when missing, so a
  service can name /var/lib/runicgateway on a host where nothing made it yet.
- 22 unit tests covering argument parsing, path anchoring, token persistence, the
  generated config template, and the --print-config document.

No protocol change: PROTOCOL_VERSION stays 3.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech approved these changes 2026-08-04 15:48:06 +00:00
whitlocktech merged commit 654a08add4 into main 2026-08-04 15:48:12 +00:00
whitlocktech deleted branch feat/installable-cli 2026-08-04 15:48:13 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/link#24
No description provided.