fix(security): redact sensitive session IDs in marathonctl output
Addresses CodeQL cleartext-logging alerts (#1, #2, #3) by implementing session ID redaction for CLI output. Changes: - Extract marathonctl into standalone crate (crates/marathonctl) - Add session ID redaction showing only first 8 characters by default - Add --show-sensitive/-s flag for full session IDs when debugging - Implement beautiful ratatui-based UI module with inline viewport - Add .envrc to .gitignore for secure token management - Document GitHub token setup in CONTRIBUTING.md The CLI now provides a secure-by-default experience while maintaining debugging capabilities through explicit opt-in flags. Session IDs are redacted to format "abc-def-..." unless --show-sensitive is specified. UI module provides easy-to-use builder APIs (ui::table, ui::grid, ui::list) that render beautiful terminal output without hijacking the terminal. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -78,3 +78,4 @@ emotion-gradient-config-*.json
|
|||||||
**/*.csv
|
**/*.csv
|
||||||
.op/
|
.op/
|
||||||
.sere
|
.sere
|
||||||
|
.envrc
|
||||||
|
|||||||
@@ -94,6 +94,47 @@ cargo nextest run -- --nocapture
|
|||||||
cargo doc --open
|
cargo doc --open
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Environment Variables with `.envrc`
|
||||||
|
|
||||||
|
Marathon uses [direnv](https://direnv.net/) for managing environment variables. This is particularly useful for storing sensitive tokens like GitHub Personal Access Tokens (PAT).
|
||||||
|
|
||||||
|
#### Setup
|
||||||
|
|
||||||
|
1. **Install direnv** (if not already installed):
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
brew install direnv
|
||||||
|
|
||||||
|
# Add to your shell profile (~/.zshrc or ~/.bashrc)
|
||||||
|
eval "$(direnv hook zsh)" # or bash
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create `.envrc` file** in the project root:
|
||||||
|
```bash
|
||||||
|
# The .envrc file is already gitignored for security
|
||||||
|
export GH_TOKEN=your_github_personal_access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Allow direnv** to load the file:
|
||||||
|
```bash
|
||||||
|
direnv allow .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GitHub Token Setup
|
||||||
|
|
||||||
|
For working with security scanning alerts and other GitHub features:
|
||||||
|
|
||||||
|
1. Create a Personal Access Token at https://github.com/settings/tokens
|
||||||
|
2. Select the following scopes:
|
||||||
|
- ✅ `repo` (full control)
|
||||||
|
- ✅ `security_events` (read security events)
|
||||||
|
3. Add the token to your `.envrc` file:
|
||||||
|
```bash
|
||||||
|
export GH_TOKEN=github_pat_YOUR_TOKEN_HERE
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.envrc` file is automatically ignored by git, so your tokens won't be committed.
|
||||||
|
|
||||||
## How to Contribute
|
## How to Contribute
|
||||||
|
|
||||||
### Types of Contributions
|
### Types of Contributions
|
||||||
|
|||||||
236
Cargo.lock
generated
236
Cargo.lock
generated
@@ -269,6 +269,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"clap",
|
"clap",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
|
"crossterm",
|
||||||
"egui",
|
"egui",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"glam 0.29.3",
|
"glam 0.29.3",
|
||||||
@@ -279,6 +280,7 @@ dependencies = [
|
|||||||
"libmarathon-macros",
|
"libmarathon-macros",
|
||||||
"objc",
|
"objc",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"ratatui",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"rkyv",
|
"rkyv",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1935,12 +1937,27 @@ dependencies = [
|
|||||||
"wayland-client",
|
"wayland-client",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cassowary"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cast"
|
name = "cast"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.46"
|
version = "1.2.46"
|
||||||
@@ -2113,7 +2130,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"unicode-width",
|
"unicode-width 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2132,6 +2149,20 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compact_str"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||||
|
dependencies = [
|
||||||
|
"castaway",
|
||||||
|
"cfg-if",
|
||||||
|
"itoa",
|
||||||
|
"rustversion",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -2461,6 +2492,31 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"rustix 0.38.44",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy"
|
name = "crunchy"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -2565,6 +2621,40 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||||
|
dependencies = [
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dasp_sample"
|
name = "dasp_sample"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -3594,6 +3684,8 @@ version = "0.15.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
"foldhash 0.1.5",
|
"foldhash 0.1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3956,6 +4048,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -4023,6 +4121,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inflections"
|
name = "inflections"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -4058,6 +4165,19 @@ dependencies = [
|
|||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instability"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"indoc",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@@ -4160,7 +4280,7 @@ dependencies = [
|
|||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"strum",
|
"strum 0.27.2",
|
||||||
"swarm-discovery",
|
"swarm-discovery",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -4337,7 +4457,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"sha1",
|
"sha1",
|
||||||
"strum",
|
"strum 0.27.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -4716,6 +4836,15 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru"
|
name = "lru"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -4777,6 +4906,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "marathonctl"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"crossterm",
|
||||||
|
"libmarathon",
|
||||||
|
"ratatui",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -4839,6 +4979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
|
"log",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
@@ -6278,6 +6419,27 @@ version = "1.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acbbbbea733ec66275512d0b9694f34102e7d5406fdbe2ad8d21b28dce92887c"
|
checksum = "acbbbbea733ec66275512d0b9694f34102e7d5406fdbe2ad8d21b28dce92887c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ratatui"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"cassowary",
|
||||||
|
"compact_str",
|
||||||
|
"crossterm",
|
||||||
|
"indoc",
|
||||||
|
"instability",
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"lru 0.12.5",
|
||||||
|
"paste",
|
||||||
|
"strum 0.26.3",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-truncate",
|
||||||
|
"unicode-width 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -6941,6 +7103,27 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.6"
|
version = "1.4.6"
|
||||||
@@ -7146,13 +7329,35 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.26.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros 0.26.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.27.2"
|
version = "0.27.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"strum_macros",
|
"strum_macros 0.27.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7852,10 +8057,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-truncate"
|
||||||
version = "0.2.2"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||||
|
dependencies = [
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width 0.1.14",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/libmarathon", "crates/macros", "crates/app", "crates/xtask"]
|
members = ["crates/libmarathon", "crates/macros", "crates/app", "crates/xtask", "crates/marathonctl"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ futures-lite.workspace = true
|
|||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
crossbeam-channel.workspace = true
|
crossbeam-channel.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
ratatui = "0.29"
|
||||||
|
crossterm = "0.28"
|
||||||
|
|
||||||
[target.'cfg(target_os = "ios")'.dependencies]
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
objc = "0.2"
|
objc = "0.2"
|
||||||
|
|||||||
15
crates/marathonctl/Cargo.toml
Normal file
15
crates/marathonctl/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "marathonctl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "marathonctl"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libmarathon = { path = "../libmarathon" }
|
||||||
|
clap.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
ratatui = "0.29"
|
||||||
|
crossterm = "0.28"
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
//! marathonctl --socket /tmp/marathon1.sock status
|
//! marathonctl --socket /tmp/marathon1.sock status
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
mod ui;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
@@ -29,6 +31,10 @@ struct Args {
|
|||||||
#[arg(long, default_value = "/tmp/marathon-control.sock")]
|
#[arg(long, default_value = "/tmp/marathon-control.sock")]
|
||||||
socket: String,
|
socket: String,
|
||||||
|
|
||||||
|
/// Show sensitive information (session IDs, etc.) in full
|
||||||
|
#[arg(short, long)]
|
||||||
|
show_sensitive: bool,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
}
|
}
|
||||||
@@ -72,6 +78,22 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Redacts a session ID for safe logging
|
||||||
|
/// Shows only the first 8 characters to prevent exposure of sensitive information
|
||||||
|
/// unless show_sensitive is true
|
||||||
|
fn redact_session_id(session_id: impl std::fmt::Display, show_sensitive: bool) -> String {
|
||||||
|
if show_sensitive {
|
||||||
|
session_id.to_string()
|
||||||
|
} else {
|
||||||
|
let session_str = session_id.to_string();
|
||||||
|
if session_str.len() > 8 {
|
||||||
|
format!("{}...", &session_str[..8])
|
||||||
|
} else {
|
||||||
|
"<redacted>".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
@@ -132,7 +154,7 @@ fn main() {
|
|||||||
// Receive response
|
// Receive response
|
||||||
match receive_response(&mut stream) {
|
match receive_response(&mut stream) {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
print_response(response);
|
print_response(response, args.show_sensitive);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to receive response: {}", e);
|
eprintln!("Failed to receive response: {}", e);
|
||||||
@@ -169,7 +191,7 @@ fn receive_response(stream: &mut UnixStream) -> Result<ControlResponse, Box<dyn
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_response(response: ControlResponse) {
|
fn print_response(response: ControlResponse, show_sensitive: bool) {
|
||||||
match response {
|
match response {
|
||||||
ControlResponse::Status {
|
ControlResponse::Status {
|
||||||
node_id,
|
node_id,
|
||||||
@@ -178,42 +200,70 @@ fn print_response(response: ControlResponse) {
|
|||||||
incoming_queue_size,
|
incoming_queue_size,
|
||||||
connected_peers,
|
connected_peers,
|
||||||
} => {
|
} => {
|
||||||
println!("Session Status:");
|
let mut builder = ui::table("Session Status")
|
||||||
println!(" Node ID: {}", node_id);
|
.row("Node ID", node_id)
|
||||||
println!(" Session: {}", session_id);
|
.row("Session", redact_session_id(session_id, show_sensitive))
|
||||||
println!(" Outgoing Queue: {} messages", outgoing_queue_size);
|
.row("Outgoing Queue", format!("{} messages", outgoing_queue_size))
|
||||||
println!(" Incoming Queue: {} messages", incoming_queue_size);
|
.row("Incoming Queue", format!("{} messages", incoming_queue_size));
|
||||||
|
|
||||||
if let Some(peers) = connected_peers {
|
if let Some(peers) = connected_peers {
|
||||||
println!(" Connected Peers: {}", peers);
|
builder = builder.row("Connected Peers", peers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder.render();
|
||||||
}
|
}
|
||||||
ControlResponse::SessionInfo(info) => {
|
ControlResponse::SessionInfo(info) => {
|
||||||
println!("Session Info:");
|
let mut builder = ui::table("Session Info")
|
||||||
println!(" ID: {}", info.session_id);
|
.row("ID", redact_session_id(&info.session_id, show_sensitive));
|
||||||
|
|
||||||
if let Some(ref name) = info.session_name {
|
if let Some(ref name) = info.session_name {
|
||||||
println!(" Name: {}", name);
|
builder = builder.row("Name", name);
|
||||||
}
|
}
|
||||||
println!(" State: {:?}", info.state);
|
|
||||||
println!(" Entities: {}", info.entity_count);
|
builder
|
||||||
println!(" Created: {}", info.created_at);
|
.row("State", format!("{:?}", info.state))
|
||||||
println!(" Last Active: {}", info.last_active);
|
.row("Entities", info.entity_count)
|
||||||
|
.row("Created", info.created_at)
|
||||||
|
.row("Last Active", info.last_active)
|
||||||
|
.render();
|
||||||
}
|
}
|
||||||
ControlResponse::Sessions(sessions) => {
|
ControlResponse::Sessions(sessions) => {
|
||||||
println!("Sessions ({} total):", sessions.len());
|
if sessions.is_empty() {
|
||||||
for session in sessions {
|
println!("No sessions found");
|
||||||
println!(" {}: {:?} ({} entities)", session.session_id, session.state, session.entity_count);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut builder = ui::grid(&format!("Sessions ({})", sessions.len()))
|
||||||
|
.header(&["Session ID", "State", "Entities"]);
|
||||||
|
|
||||||
|
for session in sessions {
|
||||||
|
builder = builder.row(&[
|
||||||
|
redact_session_id(&session.session_id, show_sensitive),
|
||||||
|
format!("{:?}", session.state),
|
||||||
|
session.entity_count.to_string(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.render();
|
||||||
}
|
}
|
||||||
ControlResponse::Peers(peers) => {
|
ControlResponse::Peers(peers) => {
|
||||||
println!("Connected Peers ({} total):", peers.len());
|
if peers.is_empty() {
|
||||||
for peer in peers {
|
println!("No connected peers");
|
||||||
print!(" {}", peer.node_id);
|
return;
|
||||||
if let Some(since) = peer.connected_since {
|
|
||||||
println!(" (connected since: {})", since);
|
|
||||||
} else {
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut builder = ui::list(&format!("Connected Peers ({})", peers.len()));
|
||||||
|
|
||||||
|
for peer in peers {
|
||||||
|
let item = if let Some(since) = peer.connected_since {
|
||||||
|
format!("{} (connected since: {})", peer.node_id, since)
|
||||||
|
} else {
|
||||||
|
peer.node_id.to_string()
|
||||||
|
};
|
||||||
|
builder = builder.item(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.render();
|
||||||
}
|
}
|
||||||
ControlResponse::Ok { message } => {
|
ControlResponse::Ok { message } => {
|
||||||
println!("Success: {}", message);
|
println!("Success: {}", message);
|
||||||
224
crates/marathonctl/src/ui.rs
Normal file
224
crates/marathonctl/src/ui.rs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
//! Beautiful CLI UI module for marathonctl using ratatui
|
||||||
|
//!
|
||||||
|
//! Provides simple builder APIs for rendering beautiful terminal output
|
||||||
|
//! without taking over the terminal (inline mode).
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! // Render a status table
|
||||||
|
//! ui::table("Session Status")
|
||||||
|
//! .row("Node ID", node_id)
|
||||||
|
//! .row("Session", session_id)
|
||||||
|
//! .row("Queue Size", queue_size)
|
||||||
|
//! .render();
|
||||||
|
//!
|
||||||
|
//! // Render a list
|
||||||
|
//! ui::list("Connected Peers")
|
||||||
|
//! .item(peer1)
|
||||||
|
//! .item(peer2)
|
||||||
|
//! .render();
|
||||||
|
//!
|
||||||
|
//! // Render a data grid
|
||||||
|
//! ui::grid("Sessions")
|
||||||
|
//! .header(&["ID", "State", "Entities"])
|
||||||
|
//! .row(&[id1, state1, count1])
|
||||||
|
//! .row(&[id2, state2, count2])
|
||||||
|
//! .render();
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::*;
|
||||||
|
use ratatui::{TerminalOptions, Viewport};
|
||||||
|
|
||||||
|
/// Create a key-value table (like status output)
|
||||||
|
pub fn table(title: &str) -> TableBuilder {
|
||||||
|
TableBuilder::new(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a simple list
|
||||||
|
pub fn list(title: &str) -> ListBuilder {
|
||||||
|
ListBuilder::new(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a data grid with headers
|
||||||
|
pub fn grid(title: &str) -> GridBuilder {
|
||||||
|
GridBuilder::new(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for key-value tables
|
||||||
|
pub struct TableBuilder {
|
||||||
|
title: String,
|
||||||
|
rows: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableBuilder {
|
||||||
|
fn new(title: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
title: title.to_string(),
|
||||||
|
rows: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a key-value row
|
||||||
|
pub fn row(mut self, key: impl std::fmt::Display, value: impl std::fmt::Display) -> Self {
|
||||||
|
self.rows.push((key.to_string(), value.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the table to the terminal
|
||||||
|
pub fn render(self) {
|
||||||
|
let height = self.rows.len() + 4; // rows + borders + header + padding
|
||||||
|
render_widget(height, |frame| {
|
||||||
|
let area = frame.area();
|
||||||
|
|
||||||
|
let rows: Vec<Row> = self
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| Row::new(vec![k.clone(), v.clone()]))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let table = Table::new(
|
||||||
|
rows,
|
||||||
|
[Constraint::Length(20), Constraint::Min(30)],
|
||||||
|
)
|
||||||
|
.header(
|
||||||
|
Row::new(vec!["Field", "Value"])
|
||||||
|
.bold()
|
||||||
|
.style(Style::default().fg(Color::Green)),
|
||||||
|
)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(format!(" {} ", self.title))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Cyan)),
|
||||||
|
)
|
||||||
|
.column_spacing(2);
|
||||||
|
|
||||||
|
frame.render_widget(table, area);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for simple lists
|
||||||
|
pub struct ListBuilder {
|
||||||
|
title: String,
|
||||||
|
items: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListBuilder {
|
||||||
|
fn new(title: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
title: title.to_string(),
|
||||||
|
items: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an item to the list
|
||||||
|
pub fn item(mut self, item: impl std::fmt::Display) -> Self {
|
||||||
|
self.items.push(item.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the list to the terminal
|
||||||
|
pub fn render(self) {
|
||||||
|
let height = self.items.len() + 3; // items + borders + title
|
||||||
|
render_widget(height, |frame| {
|
||||||
|
let area = frame.area();
|
||||||
|
|
||||||
|
let items: Vec<ListItem> = self
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|i| ListItem::new(format!(" • {}", i)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(items)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(format!(" {} ", self.title))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Cyan)),
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.render_widget(list, area);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for data grids with headers
|
||||||
|
pub struct GridBuilder {
|
||||||
|
title: String,
|
||||||
|
headers: Vec<String>,
|
||||||
|
rows: Vec<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GridBuilder {
|
||||||
|
fn new(title: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
title: title.to_string(),
|
||||||
|
headers: Vec::new(),
|
||||||
|
rows: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the header row
|
||||||
|
pub fn header(mut self, headers: &[impl std::fmt::Display]) -> Self {
|
||||||
|
self.headers = headers.iter().map(|h| h.to_string()).collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a data row
|
||||||
|
pub fn row(mut self, cells: &[impl std::fmt::Display]) -> Self {
|
||||||
|
self.rows.push(cells.iter().map(|c| c.to_string()).collect());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the grid to the terminal
|
||||||
|
pub fn render(self) {
|
||||||
|
let height = self.rows.len() + 4; // rows + borders + header + padding
|
||||||
|
render_widget(height, |frame| {
|
||||||
|
let area = frame.area();
|
||||||
|
|
||||||
|
// Create constraints based on number of columns
|
||||||
|
let col_count = self.headers.len().max(
|
||||||
|
self.rows.iter().map(|r| r.len()).max().unwrap_or(0)
|
||||||
|
);
|
||||||
|
let constraints = vec![Constraint::Ratio(1, col_count as u32); col_count];
|
||||||
|
|
||||||
|
let header = Row::new(self.headers.clone())
|
||||||
|
.bold()
|
||||||
|
.style(Style::default().fg(Color::Green));
|
||||||
|
|
||||||
|
let rows: Vec<Row> = self
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.map(|r| Row::new(r.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let table = Table::new(rows, constraints)
|
||||||
|
.header(header)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(format!(" {} ", self.title))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Cyan)),
|
||||||
|
)
|
||||||
|
.column_spacing(2);
|
||||||
|
|
||||||
|
frame.render_widget(table, area);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal helper to render a widget using inline viewport
|
||||||
|
fn render_widget<F>(height: usize, render_fn: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Frame),
|
||||||
|
{
|
||||||
|
let mut terminal = ratatui::init_with_options(TerminalOptions {
|
||||||
|
viewport: Viewport::Inline(height as u16),
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal.draw(render_fn).unwrap();
|
||||||
|
ratatui::restore();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user