Skip to content

Bootstrap standalone CLI and config foundation#112

Closed
qarlus wants to merge 1 commit intodevfrom
feature/standalone-cli-config-bootstrap
Closed

Bootstrap standalone CLI and config foundation#112
qarlus wants to merge 1 commit intodevfrom
feature/standalone-cli-config-bootstrap

Conversation

@qarlus
Copy link
Copy Markdown
Collaborator

@qarlus qarlus commented Apr 5, 2026

Objective

Bootstrap a runnable standalone crate foundation with CLI and configuration loading so later runtime slices can add windowing, PTY wiring, and renderer integration on top of stable argument/config contracts.

Scope

  • Add new workspace member crates/iris-standalone.
  • Implement CLI argument parsing in src/cli.rs.
  • Implement TOML/default configuration loading in src/config.rs.
  • Wire startup config resolution in src/main.rs.
  • Add unit tests for CLI parsing and config loading/error/default behavior.
  • Update changelog and standalone planning doc status.

API and Behavior Changes

  1. API surface change
  • Added standalone binary crate iris-standalone.
  • Added internal CLI contract (Args, CommandKind) for command/config/theme/connection options.
  • Added internal config contract (Config and nested sections) with default values and file loading.
  1. Runtime behavior change
  • iris-standalone now parses CLI arguments and resolves configuration from explicit --config path or default path.
  • Missing config files are treated as defaults rather than startup failures.
  • Invalid config files fail fast with path-scoped parse errors.
  1. Error behavior change
  • Added structured config load errors (LoadError::Read and LoadError::Parse) with source-path context.

Backward Compatibility

  • Additive/non-breaking.
  • Existing crates and APIs are unchanged; this introduces a new standalone crate only.

Test Coverage

Added tests in crates/iris-standalone/src/cli.rs:

  • cli_parses_command_and_config_flags
  • cli_parses_subcommand_and_default_baud

Added tests in crates/iris-standalone/src/config.rs:

  • config_load_returns_defaults_when_file_is_missing
  • config_load_parses_partial_file_and_applies_defaults
  • config_load_returns_parse_error_for_invalid_toml
  • default_path_uses_iris_config_toml_suffix

Coverage strategy:

  • Happy path: parses expected CLI/config inputs.
  • Error/edge: invalid TOML path-scoped errors and missing-file default fallback.
  • Platform-specific: default shell fallback is OS-aware in config defaults.

Verification

  • cargo fmt --all (pass)
  • cargo clippy --all-targets -- -D warnings (pass)
  • cargo test --all (pass)

Files Changed

  • Cargo.toml
  • Cargo.lock
  • crates/iris-standalone/Cargo.toml
  • crates/iris-standalone/src/main.rs
  • crates/iris-standalone/src/cli.rs
  • crates/iris-standalone/src/config.rs
  • docs/phases/06.md
  • CHANGELOG.md

Notes

  • This PR intentionally focuses on standalone bootstrap contracts only; window/event-loop/PTY/renderer runtime integration will follow in subsequent slices.

Summary by CodeRabbit

  • New Features

    • Introduced standalone terminal host bootstrap with CLI argument parsing and configuration support.
    • Added configuration loading via TOML files with sensible defaults.
    • Added CLI flags for command execution, working directory, theme, SSH, serial device, and baud rate settings.
    • Added subcommand support for themes, fonts, and configuration management.
  • Tests

    • Added unit tests for CLI argument and configuration loading.
  • Documentation

    • Updated phase documentation with current standalone bootstrap status and remaining integration focus areas.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

A new iris-standalone crate is introduced with CLI argument parsing via clap, TOML-based configuration loading with fallback defaults, and a startup entrypoint that resolves CLI args and config before runtime initialization. Workspace configuration is updated and project documentation reflects the completed bootstrap scaffolding.

Changes

Cohort / File(s) Summary
Workspace & Crate Setup
Cargo.toml, crates/iris-standalone/Cargo.toml
Added iris-standalone crate to workspace members; new crate manifest declares dependencies on clap, dirs, serde, thiserror, and toml with tempfile as dev-dependency.
CLI Argument Parsing
crates/iris-standalone/src/cli.rs
Introduced Args struct with optional flags for command execution, working directory, config path, theme, SSH target, and serial device; includes CommandKind subcommand enum with Themes, Fonts, and Config variants; unit tests verify flag parsing and subcommand handling.
Configuration Loading & Defaults
crates/iris-standalone/src/config.rs
Added Config structure with nested TerminalConfig, FontConfig, and WindowConfig subtypes using serde deserialization; implemented load() function resolving optional config paths with fallback to default_path(); includes LoadError enum for I/O and parse failures; unit tests cover missing files, partial TOML merging with defaults, and error classification.
Startup Entrypoint
crates/iris-standalone/src/main.rs
Simple entrypoint wiring CLI parsing and config loading; returns ExitCode::SUCCESS on successful config load, exits with code 1 and logs errors on failure.
Documentation
CHANGELOG.md, docs/phases/06.md
Changelog entry summarizing new crate scaffolding; phase documentation updated with completion status and remaining integration focus (window/event-loop, PTY session, input routing, platform packaging).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hops through CLI fields so bright,
Config paths resolved with defaults just right,
From args to TOML, the startup takes flight,
With error handling tucked safe out of sight, 🐰✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Bootstrap standalone CLI and config foundation' directly and clearly summarizes the main change: establishing the foundation for the standalone crate with CLI and config loading.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/standalone-cli-config-bootstrap

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/iris-standalone/src/config.rs`:
- Around line 82-89: Remove the pre-check using resolved_path.exists() and
instead attempt to read the file with fs::read_to_string(&resolved_path)
directly; on Ok parse as before, and on Err match the io::Error.kind() — if
ErrorKind::NotFound then return Ok(Config::default()), otherwise convert the
error into LoadError::Read (including path: resolved_path.clone(), source:
Box::new(err)) so permission and other I/O errors propagate; update any
surrounding logic that assumed the exists() check accordingly (symbols:
resolved_path, fs::read_to_string, LoadError::Read, Config::default(),
ErrorKind::NotFound).

In `@crates/iris-standalone/src/main.rs`:
- Around line 8-10: The code currently calls cli::Args::parse() and then ignores
any parsed subcommands, letting `themes`, `fonts`, or `config` behave as no-ops;
update the startup flow to explicitly handle or reject subcommands by matching
on the parsed Args before calling config::load(): inspect the subcommand
enum/field on the result of cli::Args::parse() (e.g., args.subcommand or the
specific variant names from the CLI type), add branches that either call the
appropriate handler or return an explicit unimplemented/error ExitCode for
unsupported subcommands, and only fall back to config::load() in the intended
default branch so subcommands are not silently ignored.

In `@docs/phases/06.md`:
- Around line 24-26: The Status section claims unit coverage for CLI/config in
crates/iris-standalone, but the Unit Tests checklist still shows those items
unchecked—synchronize them by updating the Unit Tests checklist to check the CLI
parsing and configuration loading/defaulting/error-path items (or conversely
change the Status text to reflect remaining gaps); locate references to
crates/iris-standalone, cli.rs, config.rs, main.rs and the "Unit Tests"
checklist in this document and make the entries consistent so the status and
checklist no longer conflict.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ef69beb8-036a-4462-8ddb-124ae3dbcad1

📥 Commits

Reviewing files that changed from the base of the PR and between 9e3d533 and 3f0c19b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • CHANGELOG.md
  • Cargo.toml
  • crates/iris-standalone/Cargo.toml
  • crates/iris-standalone/src/cli.rs
  • crates/iris-standalone/src/config.rs
  • crates/iris-standalone/src/main.rs
  • docs/phases/06.md

Comment on lines +82 to +89
if !resolved_path.exists() {
return Ok(Config::default());
}

let content = fs::read_to_string(&resolved_path).map_err(|source| LoadError::Read {
path: resolved_path.clone(),
source: Box::new(source),
})?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Rust's standard library, what does std::path::Path::exists return on permission errors, and what API pattern is recommended instead for distinguishing "missing file" from other I/O failures?

💡 Result:

std::path::Path::exists returns false on permission errors. The recommended API pattern to distinguish "missing file" from other I/O failures is to use std::path::Path::try_exists (stable since Rust 1.63), which returns Result<bool> and propagates io::Error for permission denied and similar issues, or std::fs::metadata (or std::fs::exists since Rust 1.81), checking the error kind to differentiate NotFound from PermissionDenied.

Citations:


🏁 Script executed:

# Find the config.rs file and check its content
find crates -name "config.rs" -type f

Repository: herma-sh/iris

Length of output: 93


🏁 Script executed:

# Read the specific lines mentioned in the review
cat -n crates/iris-standalone/src/config.rs | sed -n '75,95p'

Repository: herma-sh/iris

Length of output: 814


🏁 Script executed:

# Check for rust version constraints
cat Cargo.toml | head -40

Repository: herma-sh/iris

Length of output: 339


🏁 Script executed:

# Check if there's a rust-toolchain file
cat rust-toolchain.toml 2>/dev/null || cat rust-toolchain 2>/dev/null || echo "No rust-toolchain file found"

Repository: herma-sh/iris

Length of output: 85


🏁 Script executed:

# Check all Cargo.toml files for rust-version or MSRV specification
rg "rust-version|MSRV" --type toml .

Repository: herma-sh/iris

Length of output: 39


🏁 Script executed:

# Check the specific crate's Cargo.toml
cat crates/iris-standalone/Cargo.toml | head -30

Repository: herma-sh/iris

Length of output: 408


🏁 Script executed:

# Check if try_exists is used anywhere in the codebase
rg "try_exists" .

Repository: herma-sh/iris

Length of output: 39


🏁 Script executed:

# Check what error handling patterns are used elsewhere in the codebase
rg "ErrorKind::" crates/iris-standalone/src/config.rs

Repository: herma-sh/iris

Length of output: 39


🏁 Script executed:

# Check for test files related to config
find crates/iris-standalone -name "*test*" -o -name "tests" -type d

Repository: herma-sh/iris

Length of output: 39


🏁 Script executed:

# Look for tests in the config.rs file itself
cat crates/iris-standalone/src/config.rs | tail -50

Repository: herma-sh/iris

Length of output: 1643


🏁 Script executed:

# Check the full load function and surrounding context
cat -n crates/iris-standalone/src/config.rs | sed -n '60,95p'

Repository: herma-sh/iris

Length of output: 1261


🏁 Script executed:

# Search for LoadError definition and usage
rg "LoadError" crates/iris-standalone/src/config.rs -A 3 -B 1

Repository: herma-sh/iris

Length of output: 1168


Replace the exists() pre-check with a read-first pattern to surface I/O errors.

The current code at line 82 uses exists() to guard the read, but exists() returns false for permission denied and other I/O failures, silently falling back to defaults instead of reporting actual errors. Read the file first and only default on ErrorKind::NotFound, allowing permission and other I/O failures to propagate as LoadError::Read.

Recommended pattern
 pub(crate) fn load(path: Option<&Path>) -> Result<Config, LoadError> {
     let resolved_path = path.map_or_else(default_path, Path::to_path_buf);
-    if !resolved_path.exists() {
-        return Ok(Config::default());
-    }
-
-    let content = fs::read_to_string(&resolved_path).map_err(|source| LoadError::Read {
-        path: resolved_path.clone(),
-        source: Box::new(source),
-    })?;
+    let content = match fs::read_to_string(&resolved_path) {
+        Ok(content) => content,
+        Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
+            return Ok(Config::default());
+        }
+        Err(source) => {
+            return Err(LoadError::Read {
+                path: resolved_path.clone(),
+                source: Box::new(source),
+            });
+        }
+    };
     toml::from_str::<Config>(&content).map_err(|source| LoadError::Parse {
         path: resolved_path,
         source: Box::new(source),
     })
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !resolved_path.exists() {
return Ok(Config::default());
}
let content = fs::read_to_string(&resolved_path).map_err(|source| LoadError::Read {
path: resolved_path.clone(),
source: Box::new(source),
})?;
let content = match fs::read_to_string(&resolved_path) {
Ok(content) => content,
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
return Ok(Config::default());
}
Err(source) => {
return Err(LoadError::Read {
path: resolved_path.clone(),
source: Box::new(source),
});
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/iris-standalone/src/config.rs` around lines 82 - 89, Remove the
pre-check using resolved_path.exists() and instead attempt to read the file with
fs::read_to_string(&resolved_path) directly; on Ok parse as before, and on Err
match the io::Error.kind() — if ErrorKind::NotFound then return
Ok(Config::default()), otherwise convert the error into LoadError::Read
(including path: resolved_path.clone(), source: Box::new(err)) so permission and
other I/O errors propagate; update any surrounding logic that assumed the
exists() check accordingly (symbols: resolved_path, fs::read_to_string,
LoadError::Read, Config::default(), ErrorKind::NotFound).

Comment on lines +8 to +10
let args = cli::Args::parse();
match config::load(args.config.as_deref()) {
Ok(_) => ExitCode::SUCCESS,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle parsed subcommands explicitly (or reject them for now).

At Line 8, subcommands are parsed but ignored, so themes/fonts/config currently behave like no-op aliases. Consider a temporary explicit unimplemented branch to avoid ambiguous UX.

Suggested guard until subcommand handlers land
 fn main() -> ExitCode {
     let args = cli::Args::parse();
+    if let Some(command) = args.subcommand.as_ref() {
+        eprintln!("subcommand '{command:?}' is not implemented yet");
+        return ExitCode::from(2);
+    }
     match config::load(args.config.as_deref()) {
         Ok(_) => ExitCode::SUCCESS,
         Err(error) => {
             eprintln!("{error}");
             ExitCode::from(1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/iris-standalone/src/main.rs` around lines 8 - 10, The code currently
calls cli::Args::parse() and then ignores any parsed subcommands, letting
`themes`, `fonts`, or `config` behave as no-ops; update the startup flow to
explicitly handle or reject subcommands by matching on the parsed Args before
calling config::load(): inspect the subcommand enum/field on the result of
cli::Args::parse() (e.g., args.subcommand or the specific variant names from the
CLI type), add branches that either call the appropriate handler or return an
explicit unimplemented/error ExitCode for unsupported subcommands, and only fall
back to config::load() in the intended default branch so subcommands are not
silently ignored.

Comment on lines +24 to +26
- Standalone crate scaffolding is active in `crates/iris-standalone`, including `cli.rs` argument parsing, `config.rs` TOML/default loading, and `main.rs` startup wiring that resolves configuration before runtime initialization.
- Unit coverage is active in `crates/iris-standalone` for CLI parsing and configuration loading/defaulting/error-path handling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Status section conflicts with the test checklist below.

Line 25 says CLI/config unit coverage is active, but the Unit Tests checklist later in this file still marks those items unchecked. Please sync these sections to avoid contradictory phase tracking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/phases/06.md` around lines 24 - 26, The Status section claims unit
coverage for CLI/config in crates/iris-standalone, but the Unit Tests checklist
still shows those items unchecked—synchronize them by updating the Unit Tests
checklist to check the CLI parsing and configuration
loading/defaulting/error-path items (or conversely change the Status text to
reflect remaining gaps); locate references to crates/iris-standalone, cli.rs,
config.rs, main.rs and the "Unit Tests" checklist in this document and make the
entries consistent so the status and checklist no longer conflict.

@qarlus
Copy link
Copy Markdown
Collaborator Author

qarlus commented Apr 5, 2026

Closing for branch cleanup; this work is not being carried forward in this promotion cycle.

@qarlus qarlus closed this Apr 5, 2026
@qarlus qarlus deleted the feature/standalone-cli-config-bootstrap branch April 5, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant