Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CLI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: rustup toolchain install 1.93.0 --profile minimal --component clippy --component rustfmt
- run: cargo +1.93.0 fmt --all --check
- run: cargo +1.93.0 clippy --all-targets --locked -- -D warnings
- run: cargo +1.93.0 test --locked
- run: rustup toolchain install 1.85.0 --profile minimal
- run: cargo +1.85.0 check --all-targets --locked
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[package]
name = "lyracore-cli"
version = "0.1.0"
default-run = "lyracore"
edition = "2021"
# 1.85 is a floor the LOCKED dependency graph sets, not this crate's own code: several
# transitive crates declare `edition = "2024"`, which Cargo needs 1.85 just to parse. (The
Expand All @@ -19,6 +20,10 @@ publish = false
name = "lyracore"
path = "src/bin/main.rs"

[[bin]]
name = "lyracore-ops"
path = "src/bin/ops.rs"

[dependencies]
thiserror = "1"
serde = { version = "1", features = ["derive"] }
Expand Down
658 changes: 35 additions & 623 deletions README.md

Large diffs are not rendered by default.

216 changes: 75 additions & 141 deletions docs/commands.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions docs/contract-ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Core contract ownership

The CLI should own checkout selection, local processes, command sequencing, confirmation,
and actionable failure reports. Core should own game-content and artifact format rules.

Two contracts still need a Core interface before their CLI copies can be removed:

- `src/cmd/packages/artifact.rs` and `script.rs` reproduce canonical artifact bytes and digests.
Core's `lyracore-delta-check` validates and traces artifacts and exposes `--print-events`,
but its current output is a human report, not a versioned metadata protocol. A replacement
needs machine-readable canonical bytes, digests, package identities, counts and conflicts
from the selected checkout. Only then can the CLI remove its parser/canonicalizer without
parsing presentation text or compiling against an unrelated Core revision.
- `src/cmd/import.rs` owns detailed post-import SQL assertions for profiles, trainers, spells,
quests and geometry. Core needs a profile-aware verification command that accepts an explicit
server, database and credential source and returns a structured report with a failing exit
status. `check_world_content` checks readiness; it does not replace those detailed import
assertions. A full import script also cannot be substituted for a read-only verification step.

Keep these checks until replacements exist in the supported Core checkout. Do not maintain
both paths indefinitely: migrate their fixtures with the Core interface, switch the CLI to it,
and delete the duplicated rules. Keep credentials out of arguments and reports throughout.

The Loot Roll upgrade guard in `publish` also remains necessary while supported destinations
can carry the old schema. Remove it only when the supported upgrade floor excludes that state.

Skipping publication solely because all Rust Packages are disabled is also unsafe: an offline
disable creates no remote teardown record, so a target may still run that Package's code.
Keep the inventory-wide publication trigger until per-target compiled-module identity can prove
that disabled Rust has already been removed.
41 changes: 41 additions & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Host operations

Host administration uses `lyracore-ops`, separate from the developer CLI. The previous
`lyracore service reconcile` command now prints the replacement command and exits without changes.

From a checkout of this CLI at the revision you want to install:

```bash
cargo install --locked --path . --bin lyracore-ops
```

Then, from the selected **Core** checkout, run the installed binary as root. Use its absolute
installed path if root's PATH does not contain your Cargo bin directory:

```bash
sudo "$HOME/.cargo/bin/lyracore-ops" service reconcile
```

The tool reads `deploy/systemd/spacetimedb-standalone.service` from that Core checkout.
Update and review the checkout separately, as its owner, before reconciliation. This command
does not fetch, reset, build the gateway, publish a module, or perform a schema migration.

Reconciliation performs these steps:

1. Verify root privileges with `id -u` and read the selected unit.
2. Check the unit's account, executable, data directory, log directory and required host tools.
Missing prerequisites are named and refused; the tool does not create them.
3. Refuse another active service claiming the same data directory or listen address.
4. Install the unit into `/etc/systemd/system`, reload systemd, enable it and restart it.
5. Verify the effective start command, active state, descriptor limit and stderr destination
against the tracked unit, including systemd overrides.

The database directory is checked for existence, never created, moved or deleted.
Every reconciliation restarts the node and causes a short outage. Failures identify which
step stopped; reconciliation is not a transaction that rolls back a failed restart.

`lyracore production status` remains a read-only developer command for an explicit topology.

The `deploy/` directory also contains the history-prune, disk-guard and diagnostic-capture
scripts and their systemd units. Their installation remains described by Core's
`docs/danger-zones.md` and `docs/operations/disk-policy.md`.
21 changes: 12 additions & 9 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ fn main() {
// bug — so on failure only (never on success, and never for any other command) it gets one
// extra line pointing at `doctor`. Parsed here, ahead of `run`, so a bad-flags usage error
// (which never reaches `dev up`'s own logic) doesn't earn the hint.
let is_dev_up = matches!(Command::parse(&args), Ok(Command::DevUp { .. }));
std::process::exit(match run(&args) {
let command = Command::parse(&args);
let is_dev_up = matches!(&command, Ok(Command::DevUp { .. }));
std::process::exit(match command.and_then(run) {
Ok(code) => code,
Err(e) => {
eprintln!("lyracore: {e}");
Expand All @@ -26,8 +27,7 @@ fn main() {
});
}

fn run(args: &[String]) -> Result<i32> {
let command = Command::parse(args)?;
fn run(command: Command) -> Result<i32> {
let runner = RealProcessRunner;
let inspector = RealProcessInspector;
// Only ever pointed at the loopback node: minting an identity and claiming the operator as it.
Expand All @@ -38,6 +38,10 @@ fn run(args: &[String]) -> Result<i32> {
println!("{}", cmd::USAGE);
Ok(EXIT_OK)
}
Command::HelpTopic(topic) => {
println!("{}", cmd::help::topic(&topic)?);
Ok(EXIT_OK)
}
Command::HelpAll => {
println!("{}", cmd::USAGE_ALL);
Ok(EXIT_OK)
Expand Down Expand Up @@ -101,9 +105,9 @@ fn run(args: &[String]) -> Result<i32> {
})
.map(|_| EXIT_OK)
}
Command::DevLogs(component) => {
Command::DevLogs { component, all } => {
let dev = cmd::dev::DevManager::new(ProjectLayout::discover()?)?;
dev.logs(component).map(|_| EXIT_OK)
dev.logs(component, all).map(|_| EXIT_OK)
}
Command::DevDown { forget } => {
let mut dev = cmd::dev::DevManager::new(ProjectLayout::discover()?)?;
Expand Down Expand Up @@ -215,9 +219,8 @@ fn run(args: &[String]) -> Result<i32> {
EXIT_OK
})
}
Command::ServiceReconcile => {
cmd::service::reconcile(&ProjectLayout::discover()?, &runner).map(|_| EXIT_OK)
Command::Update { reset_mirror } => {
cmd::update::run(&ProjectLayout::discover()?, &runner, reset_mirror).map(|_| EXIT_OK)
}
Command::Update => cmd::update::run(&ProjectLayout::discover()?, &runner).map(|_| EXIT_OK),
}
}
36 changes: 36 additions & 0 deletions src/bin/ops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use lyracore_cli::{
error::EXIT_OK, ops, proc::RealProcessRunner, project::ProjectLayout, Error, Result,
};

const USAGE: &str = "lyracore-ops: host administration\n\nUSAGE:\n lyracore-ops service reconcile\n\nInstalls the Standalone Supervisor unit from the current Core checkout and restarts\nSpacetimeDB. Requires root. Does not fetch, reset or publish source.\nUpdate and review the checkout separately before running this command.";

fn run(args: &[String]) -> Result<()> {
let args: Vec<_> = args.iter().map(String::as_str).collect();
match args.as_slice() {
[]
| ["help"]
| ["--help"]
| ["-h"]
| ["service", "--help"]
| ["service", "reconcile", "--help"] => {
println!("{USAGE}");
Ok(())
}
["service", "reconcile"] => {
ops::service::reconcile(&ProjectLayout::discover()?, &RealProcessRunner)
}
_ => Err(Error::Usage(USAGE.into())),
}
}

fn main() {
let args: Vec<_> = std::env::args().skip(1).collect();
let code = match run(&args) {
Ok(()) => EXIT_OK,
Err(error) => {
eprintln!("lyracore-ops: {error}");
error.exit_code()
}
};
std::process::exit(code);
}
10 changes: 8 additions & 2 deletions src/cmd/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ mod tests {
// error instead, and cover the value it would have printed via `Config::load` directly.
let project = checkout(&tmp);
assert!(show(&project).is_ok());
assert_eq!(Config::load(&project.config_file()).unwrap().client_data, None);
assert_eq!(
Config::load(&project.config_file()).unwrap().client_data,
None
);
}

#[test]
Expand Down Expand Up @@ -91,7 +94,10 @@ mod tests {
.to_string();
assert!(error.contains("no such directory"), "{error}");
assert!(
Config::load(&project.config_file()).unwrap().client_data.is_none(),
Config::load(&project.config_file())
.unwrap()
.client_data
.is_none(),
"a refused path must not be persisted"
);
}
Expand Down
11 changes: 5 additions & 6 deletions src/cmd/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,14 +838,13 @@ impl DevManager {

// ---- logs ----

pub fn logs(&self, component: Option<Component>) -> Result<()> {
pub fn logs(&self, component: Option<Component>, all: bool) -> Result<()> {
let components = component.map_or_else(|| Component::ALL.to_vec(), |c| vec![c]);
for component in components {
let path = self.project.log_file(component);
println!("=== {} — {} ===", component.as_str(), path.display());
match std::fs::read_to_string(&path) {
Ok(content) if content.is_empty() => println!("(empty)"),
Ok(content) => print!("{content}"),
match super::logs::show(&path, all, &mut std::io::stdout().lock()) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
println!("(no log yet — `lyracore dev up` has not started this component)");
}
Expand Down Expand Up @@ -3122,7 +3121,7 @@ mod tests {
let tmp = TempDir::new().unwrap();
let dev = DevManager::new(project(&tmp)).unwrap();
// No log files exist yet — this must not error.
assert!(dev.logs(None).is_ok());
assert!(dev.logs(Some(Component::Gateway)).is_ok());
assert!(dev.logs(None, false).is_ok());
assert!(dev.logs(Some(Component::Gateway), false).is_ok());
}
}
Loading
Loading