mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
Adopt cli-table for ANSI-aware table rendering and fix fabro ps bugs
Migrate all 7 CLI tables to cli-table, which measures column widths correctly in the presence of ANSI escape codes, fixing misaligned columns in `fabro ps`. Also fix DIRECTORY to show ~/relative paths instead of just the last component, and compute elapsed duration for running jobs instead of showing "-". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a2ad0e6645
commit
fcd229f3e9
13 changed files with 404 additions and 255 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
|
@ -516,6 +516,16 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "cli-table"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14da8d951cef7cc4f13ccc9b744d736963d57863c7e6fc33c070ea274546082c"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
|
|
@ -1381,6 +1391,7 @@ dependencies = [
|
|||
"base64",
|
||||
"bytes",
|
||||
"clap",
|
||||
"cli-table",
|
||||
"dialoguer",
|
||||
"dotenvy",
|
||||
"fabro-util",
|
||||
|
|
@ -1544,6 +1555,7 @@ dependencies = [
|
|||
"base64",
|
||||
"chrono",
|
||||
"clap",
|
||||
"cli-table",
|
||||
"console 0.15.11",
|
||||
"daytona-api-client",
|
||||
"daytona-sdk",
|
||||
|
|
@ -4876,6 +4888,15 @@ dependencies = [
|
|||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termimad"
|
||||
version = "0.34.1"
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ jsonschema = "0.42"
|
|||
chrono = { version = "0.4", features = ["clock"] }
|
||||
bollard = "0.18"
|
||||
tar = "0.4"
|
||||
cli-table = { version = "0.5", default-features = false }
|
||||
console = "0.15"
|
||||
dialoguer = "0.12"
|
||||
git2 = "0.20"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
```console
|
||||
$ fabro model
|
||||
MODEL PROVIDER ALIASES CONTEXT COST SPEED
|
||||
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
|
||||
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s
|
||||
claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s
|
||||
claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s
|
||||
gpt-5.2 openai gpt5 1m $1.8 / $14.0 65 tok/s
|
||||
gpt-5-mini openai gpt5-mini 1m $0.2 / $2.0 70 tok/s
|
||||
gpt-5.2-codex openai 1m $1.8 / $14.0 100 tok/s
|
||||
gpt-5.3-codex openai codex 1m $1.8 / $14.0 100 tok/s
|
||||
gpt-5.3-codex-spark openai codex-spark 131k - / - 1000 tok/s
|
||||
gpt-5.4 openai gpt54 1m $2.5 / $15.0 70 tok/s
|
||||
gpt-5.4-pro openai gpt54-pro 1m $30.0 / $180.0 20 tok/s
|
||||
gemini-3.1-pro-preview gemini gemini-pro 1m $2.0 / $12.0 85 tok/s
|
||||
gemini-3.1-pro-preview-customtools gemini gemini-customtools 1m $2.0 / $12.0 85 tok/s
|
||||
gemini-3-flash-preview gemini gemini-flash 1m $0.5 / $3.0 150 tok/s
|
||||
gemini-3.1-flash-lite-preview gemini gemini-flash-lite 1m $0.2 / $1.5 200 tok/s
|
||||
kimi-k2.5 kimi kimi 262k $0.6 / $3.0 50 tok/s
|
||||
glm-4.7 zai glm, glm4 203k $0.6 / $2.2 100 tok/s
|
||||
minimax-m2.5 minimax minimax 197k $0.3 / $1.2 45 tok/s
|
||||
mercury-2 inception mercury 131k $0.2 / $0.8 1000 tok/s
|
||||
|
||||
[0m [0m[0m[0m[1mMODEL [0m [0m[0m [0m[0m[0m[1mPROVIDER [0m [0m[0m [0m[0m[0m[1mALIASES [0m [0m[0m [0m[0m[0m[1mCONTEXT[0m [0m[0m [0m[0m[0m[1m COST[0m [0m[0m [0m[0m[0m[1m SPEED[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-opus-4-6 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0mopus, claude-opus [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $15.0 / $75.0[0m [0m[0m [0m[0m[0m 25 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-sonnet-4-5 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0m [0m [0m[0m [0m[0m[0m 200k[0m [0m[0m [0m[0m[0m $3.0 / $15.0[0m [0m[0m [0m[0m[0m 50 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-sonnet-4-6 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0msonnet, claude-sonnet[0m [0m[0m [0m[0m[0m 200k[0m [0m[0m [0m[0m[0m $3.0 / $15.0[0m [0m[0m [0m[0m[0m 50 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-haiku-4-5 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0mhaiku, claude-haiku [0m [0m[0m [0m[0m[0m 200k[0m [0m[0m [0m[0m[0m $0.8 / $4.0[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.2 [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mgpt5 [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $1.8 / $14.0[0m [0m[0m [0m[0m[0m 65 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5-mini [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mgpt5-mini [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $0.2 / $2.0[0m [0m[0m [0m[0m[0m 70 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.2-codex [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0m [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $1.8 / $14.0[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.3-codex [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mcodex [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $1.8 / $14.0[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.3-codex-spark [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mcodex-spark [0m [0m[0m [0m[0m[0m 131k[0m [0m[0m [0m[0m[0m - / -[0m [0m[0m [0m[0m[0m1000 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.4 [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mgpt54 [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $2.5 / $15.0[0m [0m[0m [0m[0m[0m 70 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.4-pro [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mgpt54-pro [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m$30.0 / $180.0[0m [0m[0m [0m[0m[0m 20 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgemini-3.1-pro-preview [0m [0m[0m [0m[0m[0mgemini [0m [0m[0m [0m[0m[0mgemini-pro [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $2.0 / $12.0[0m [0m[0m [0m[0m[0m 85 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgemini-3.1-pro-preview-customtools[0m [0m[0m [0m[0m[0mgemini [0m [0m[0m [0m[0m[0mgemini-customtools [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $2.0 / $12.0[0m [0m[0m [0m[0m[0m 85 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgemini-3-flash-preview [0m [0m[0m [0m[0m[0mgemini [0m [0m[0m [0m[0m[0mgemini-flash [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $0.5 / $3.0[0m [0m[0m [0m[0m[0m 150 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgemini-3.1-flash-lite-preview [0m [0m[0m [0m[0m[0mgemini [0m [0m[0m [0m[0m[0mgemini-flash-lite [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $0.2 / $1.5[0m [0m[0m [0m[0m[0m 200 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mkimi-k2.5 [0m [0m[0m [0m[0m[0mkimi [0m [0m[0m [0m[0m[0mkimi [0m [0m[0m [0m[0m[0m 262k[0m [0m[0m [0m[0m[0m $0.6 / $3.0[0m [0m[0m [0m[0m[0m 50 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mglm-4.7 [0m [0m[0m [0m[0m[0mzai [0m [0m[0m [0m[0m[0mglm, glm4 [0m [0m[0m [0m[0m[0m 203k[0m [0m[0m [0m[0m[0m $0.6 / $2.2[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mminimax-m2.5 [0m [0m[0m [0m[0m[0mminimax [0m [0m[0m [0m[0m[0mminimax [0m [0m[0m [0m[0m[0m 197k[0m [0m[0m [0m[0m[0m $0.3 / $1.2[0m [0m[0m [0m[0m[0m 45 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mmercury-2 [0m [0m[0m [0m[0m[0minception[0m [0m[0m [0m[0m[0mmercury [0m [0m[0m [0m[0m[0m 131k[0m [0m[0m [0m[0m[0m $0.2 / $0.8[0m [0m[0m [0m[0m[0m1000 tok/s[0m [0m
|
||||
[0m
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
```console
|
||||
$ fabro model list --provider anthropic
|
||||
MODEL PROVIDER ALIASES CONTEXT COST SPEED
|
||||
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
|
||||
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s
|
||||
claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s
|
||||
claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s
|
||||
|
||||
[0m [0m[0m[0m[1mMODEL [0m [0m[0m [0m[0m[0m[1mPROVIDER [0m [0m[0m [0m[0m[0m[1mALIASES [0m [0m[0m [0m[0m[0m[1mCONTEXT[0m [0m[0m [0m[0m[0m[1m COST[0m [0m[0m [0m[0m[0m[1m SPEED[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-opus-4-6 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0mopus, claude-opus [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m$15.0 / $75.0[0m [0m[0m [0m[0m[0m 25 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-sonnet-4-5[0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0m [0m [0m[0m [0m[0m[0m 200k[0m [0m[0m [0m[0m[0m $3.0 / $15.0[0m [0m[0m [0m[0m[0m 50 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-sonnet-4-6[0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0msonnet, claude-sonnet[0m [0m[0m [0m[0m[0m 200k[0m [0m[0m [0m[0m[0m $3.0 / $15.0[0m [0m[0m [0m[0m[0m 50 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-haiku-4-5 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0mhaiku, claude-haiku [0m [0m[0m [0m[0m[0m 200k[0m [0m[0m [0m[0m[0m $0.8 / $4.0[0m [0m[0m [0m[0m[0m100 tok/s[0m [0m
|
||||
[0m
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
```console
|
||||
$ fabro model list --query codex
|
||||
MODEL PROVIDER ALIASES CONTEXT COST SPEED
|
||||
gpt-5.2-codex openai 1m $1.8 / $14.0 100 tok/s
|
||||
gpt-5.3-codex openai codex 1m $1.8 / $14.0 100 tok/s
|
||||
gpt-5.3-codex-spark openai codex-spark 131k - / - 1000 tok/s
|
||||
|
||||
[0m [0m[0m[0m[1mMODEL [0m [0m[0m [0m[0m[0m[1mPROVIDER[0m [0m[0m [0m[0m[0m[1mALIASES [0m [0m[0m [0m[0m[0m[1mCONTEXT[0m [0m[0m [0m[0m[0m[1m COST[0m [0m[0m [0m[0m[0m[1m SPEED[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.2-codex [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0m [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m$1.8 / $14.0[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.3-codex [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mcodex [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m$1.8 / $14.0[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.3-codex-spark[0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mcodex-spark[0m [0m[0m [0m[0m[0m 131k[0m [0m[0m [0m[0m[0m - / -[0m [0m[0m [0m[0m[0m1000 tok/s[0m [0m
|
||||
[0m
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
```console
|
||||
$ fabro model list --query OPUS
|
||||
MODEL PROVIDER ALIASES CONTEXT COST SPEED
|
||||
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
|
||||
|
||||
[0m [0m[0m[0m[1mMODEL [0m [0m[0m [0m[0m[0m[1mPROVIDER [0m [0m[0m [0m[0m[0m[1mALIASES [0m [0m[0m [0m[0m[0m[1mCONTEXT[0m [0m[0m [0m[0m[0m[1m COST[0m [0m[0m [0m[0m[0m[1m SPEED[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-opus-4-6[0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0mopus, claude-opus[0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m$15.0 / $75.0[0m [0m[0m [0m[0m[0m25 tok/s[0m [0m
|
||||
[0m
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
```console
|
||||
$ fabro model list --query opus
|
||||
MODEL PROVIDER ALIASES CONTEXT COST SPEED
|
||||
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
|
||||
|
||||
[0m [0m[0m[0m[1mMODEL [0m [0m[0m [0m[0m[0m[1mPROVIDER [0m [0m[0m [0m[0m[0m[1mALIASES [0m [0m[0m [0m[0m[0m[1mCONTEXT[0m [0m[0m [0m[0m[0m[1m COST[0m [0m[0m [0m[0m[0m[1m SPEED[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-opus-4-6[0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0mopus, claude-opus[0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m$15.0 / $75.0[0m [0m[0m [0m[0m[0m25 tok/s[0m [0m
|
||||
[0m
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
```console
|
||||
$ fabro model list
|
||||
MODEL PROVIDER ALIASES CONTEXT COST SPEED
|
||||
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
|
||||
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s
|
||||
claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s
|
||||
claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s
|
||||
gpt-5.2 openai gpt5 1m $1.8 / $14.0 65 tok/s
|
||||
gpt-5-mini openai gpt5-mini 1m $0.2 / $2.0 70 tok/s
|
||||
gpt-5.2-codex openai 1m $1.8 / $14.0 100 tok/s
|
||||
gpt-5.3-codex openai codex 1m $1.8 / $14.0 100 tok/s
|
||||
gpt-5.3-codex-spark openai codex-spark 131k - / - 1000 tok/s
|
||||
gpt-5.4 openai gpt54 1m $2.5 / $15.0 70 tok/s
|
||||
gpt-5.4-pro openai gpt54-pro 1m $30.0 / $180.0 20 tok/s
|
||||
gemini-3.1-pro-preview gemini gemini-pro 1m $2.0 / $12.0 85 tok/s
|
||||
gemini-3.1-pro-preview-customtools gemini gemini-customtools 1m $2.0 / $12.0 85 tok/s
|
||||
gemini-3-flash-preview gemini gemini-flash 1m $0.5 / $3.0 150 tok/s
|
||||
gemini-3.1-flash-lite-preview gemini gemini-flash-lite 1m $0.2 / $1.5 200 tok/s
|
||||
kimi-k2.5 kimi kimi 262k $0.6 / $3.0 50 tok/s
|
||||
glm-4.7 zai glm, glm4 203k $0.6 / $2.2 100 tok/s
|
||||
minimax-m2.5 minimax minimax 197k $0.3 / $1.2 45 tok/s
|
||||
mercury-2 inception mercury 131k $0.2 / $0.8 1000 tok/s
|
||||
|
||||
[0m [0m[0m[0m[1mMODEL [0m [0m[0m [0m[0m[0m[1mPROVIDER [0m [0m[0m [0m[0m[0m[1mALIASES [0m [0m[0m [0m[0m[0m[1mCONTEXT[0m [0m[0m [0m[0m[0m[1m COST[0m [0m[0m [0m[0m[0m[1m SPEED[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-opus-4-6 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0mopus, claude-opus [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $15.0 / $75.0[0m [0m[0m [0m[0m[0m 25 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-sonnet-4-5 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0m [0m [0m[0m [0m[0m[0m 200k[0m [0m[0m [0m[0m[0m $3.0 / $15.0[0m [0m[0m [0m[0m[0m 50 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-sonnet-4-6 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0msonnet, claude-sonnet[0m [0m[0m [0m[0m[0m 200k[0m [0m[0m [0m[0m[0m $3.0 / $15.0[0m [0m[0m [0m[0m[0m 50 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mclaude-haiku-4-5 [0m [0m[0m [0m[0m[0manthropic[0m [0m[0m [0m[0m[0mhaiku, claude-haiku [0m [0m[0m [0m[0m[0m 200k[0m [0m[0m [0m[0m[0m $0.8 / $4.0[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.2 [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mgpt5 [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $1.8 / $14.0[0m [0m[0m [0m[0m[0m 65 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5-mini [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mgpt5-mini [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $0.2 / $2.0[0m [0m[0m [0m[0m[0m 70 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.2-codex [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0m [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $1.8 / $14.0[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.3-codex [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mcodex [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $1.8 / $14.0[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.3-codex-spark [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mcodex-spark [0m [0m[0m [0m[0m[0m 131k[0m [0m[0m [0m[0m[0m - / -[0m [0m[0m [0m[0m[0m1000 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.4 [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mgpt54 [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $2.5 / $15.0[0m [0m[0m [0m[0m[0m 70 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgpt-5.4-pro [0m [0m[0m [0m[0m[0mopenai [0m [0m[0m [0m[0m[0mgpt54-pro [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m$30.0 / $180.0[0m [0m[0m [0m[0m[0m 20 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgemini-3.1-pro-preview [0m [0m[0m [0m[0m[0mgemini [0m [0m[0m [0m[0m[0mgemini-pro [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $2.0 / $12.0[0m [0m[0m [0m[0m[0m 85 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgemini-3.1-pro-preview-customtools[0m [0m[0m [0m[0m[0mgemini [0m [0m[0m [0m[0m[0mgemini-customtools [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $2.0 / $12.0[0m [0m[0m [0m[0m[0m 85 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgemini-3-flash-preview [0m [0m[0m [0m[0m[0mgemini [0m [0m[0m [0m[0m[0mgemini-flash [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $0.5 / $3.0[0m [0m[0m [0m[0m[0m 150 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mgemini-3.1-flash-lite-preview [0m [0m[0m [0m[0m[0mgemini [0m [0m[0m [0m[0m[0mgemini-flash-lite [0m [0m[0m [0m[0m[0m 1m[0m [0m[0m [0m[0m[0m $0.2 / $1.5[0m [0m[0m [0m[0m[0m 200 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mkimi-k2.5 [0m [0m[0m [0m[0m[0mkimi [0m [0m[0m [0m[0m[0mkimi [0m [0m[0m [0m[0m[0m 262k[0m [0m[0m [0m[0m[0m $0.6 / $3.0[0m [0m[0m [0m[0m[0m 50 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mglm-4.7 [0m [0m[0m [0m[0m[0mzai [0m [0m[0m [0m[0m[0mglm, glm4 [0m [0m[0m [0m[0m[0m 203k[0m [0m[0m [0m[0m[0m $0.6 / $2.2[0m [0m[0m [0m[0m[0m 100 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mminimax-m2.5 [0m [0m[0m [0m[0m[0mminimax [0m [0m[0m [0m[0m[0mminimax [0m [0m[0m [0m[0m[0m 197k[0m [0m[0m [0m[0m[0m $0.3 / $1.2[0m [0m[0m [0m[0m[0m 45 tok/s[0m [0m
|
||||
[0m[0m [0m[0m[0mmercury-2 [0m [0m[0m [0m[0m[0minception[0m [0m[0m [0m[0m[0mmercury [0m [0m[0m [0m[0m[0m 131k[0m [0m[0m [0m[0m[0m $0.2 / $0.8[0m [0m[0m [0m[0m[0m1000 tok/s[0m [0m
|
||||
[0m
|
||||
```
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ reqwest.workspace = true
|
|||
base64.workspace = true
|
||||
bytes.workspace = true
|
||||
tokio-util.workspace = true
|
||||
cli-table.workspace = true
|
||||
clap.workspace = true
|
||||
dialoguer.workspace = true
|
||||
tracing.workspace = true
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Args, Subcommand};
|
||||
use cli_table::format::{Border, Justify, Separator};
|
||||
use cli_table::{print_stdout, Cell, CellStruct, Color, Style, Table};
|
||||
use futures::StreamExt;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
|
@ -107,30 +109,64 @@ fn format_speed(tps: Option<f64>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn print_models_table(models: &[crate::types::ModelInfo], s: &Styles) {
|
||||
println!(
|
||||
"{}",
|
||||
s.bold_dim.apply_to(format!(
|
||||
"{:<30} {:<12} {:<24} {:>10} {:>7} {:>7} {:>10}",
|
||||
"MODEL", "PROVIDER", "ALIASES", "CONTEXT", "COST", "", "SPEED",
|
||||
)),
|
||||
);
|
||||
for model in models {
|
||||
let aliases = model.aliases.join(", ");
|
||||
println!(
|
||||
"{} {} {} {:>10} {:>7} / {:<7} {}",
|
||||
s.bold.apply_to(format!("{:<30}", model.id)),
|
||||
s.dim.apply_to(format!("{:<12}", model.provider)),
|
||||
s.dim.apply_to(format!("{:<24}", aliases)),
|
||||
format_context_window(model.limits.context_window),
|
||||
format_cost(model.costs.input_cost_per_mtok),
|
||||
format_cost(model.costs.output_cost_per_mtok),
|
||||
s.cyan
|
||||
.apply_to(format!("{:>10}", format_speed(model.estimated_output_tps))),
|
||||
);
|
||||
fn color_if(use_color: bool, color: Color) -> Option<Color> {
|
||||
if use_color {
|
||||
Some(color)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn model_row(model: &crate::types::ModelInfo, use_color: bool) -> Vec<CellStruct> {
|
||||
let aliases = model.aliases.join(", ");
|
||||
let cost = format!(
|
||||
"{} / {}",
|
||||
format_cost(model.costs.input_cost_per_mtok),
|
||||
format_cost(model.costs.output_cost_per_mtok),
|
||||
);
|
||||
vec![
|
||||
model.id.clone().cell().bold(use_color),
|
||||
model
|
||||
.provider
|
||||
.clone()
|
||||
.cell()
|
||||
.foreground_color(color_if(use_color, Color::Ansi256(8))),
|
||||
aliases
|
||||
.cell()
|
||||
.foreground_color(color_if(use_color, Color::Ansi256(8))),
|
||||
format_context_window(model.limits.context_window)
|
||||
.cell()
|
||||
.justify(Justify::Right),
|
||||
cost.cell().justify(Justify::Right),
|
||||
format_speed(model.estimated_output_tps)
|
||||
.cell()
|
||||
.justify(Justify::Right)
|
||||
.foreground_color(color_if(use_color, Color::Cyan)),
|
||||
]
|
||||
}
|
||||
|
||||
fn models_title() -> Vec<CellStruct> {
|
||||
vec![
|
||||
"MODEL".cell().bold(true),
|
||||
"PROVIDER".cell().bold(true),
|
||||
"ALIASES".cell().bold(true),
|
||||
"CONTEXT".cell().bold(true).justify(Justify::Right),
|
||||
"COST".cell().bold(true).justify(Justify::Right),
|
||||
"SPEED".cell().bold(true).justify(Justify::Right),
|
||||
]
|
||||
}
|
||||
|
||||
fn print_models_table(models: &[crate::types::ModelInfo], s: &Styles) {
|
||||
let use_color = s.use_color;
|
||||
let rows: Vec<Vec<CellStruct>> = models.iter().map(|m| model_row(m, use_color)).collect();
|
||||
let table = rows
|
||||
.table()
|
||||
.title(models_title())
|
||||
.border(Border::builder().build())
|
||||
.separator(Separator::builder().build());
|
||||
let _ = print_stdout(table);
|
||||
}
|
||||
|
||||
fn read_stdin_prompt() -> Option<String> {
|
||||
let stdin = io::stdin();
|
||||
if stdin.is_terminal() {
|
||||
|
|
@ -812,46 +848,48 @@ async fn test_models_via_server(
|
|||
bail!("No models found");
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
s.bold_dim.apply_to(format!(
|
||||
"{:<30} {:<12} {:>10} {:>7} {:>7} {:>10} RESULT",
|
||||
"MODEL", "PROVIDER", "CONTEXT", "COST", "", "SPEED",
|
||||
)),
|
||||
);
|
||||
let use_color = s.use_color;
|
||||
let mut title = models_title();
|
||||
title.push("RESULT".cell().bold(true));
|
||||
|
||||
let mut rows: Vec<Vec<CellStruct>> = Vec::new();
|
||||
let mut failures = 0u32;
|
||||
for info in &models_to_test {
|
||||
eprint!("Testing {}...", info.id);
|
||||
let result = test_model_via_server(&server.client, &server.base_url, &info.id).await;
|
||||
eprintln!(" done");
|
||||
|
||||
let (status_color, status) = match result {
|
||||
Ok(resp) if resp.status == "ok" => (&s.green, "ok".to_string()),
|
||||
let (result_color, status) = match result {
|
||||
Ok(resp) if resp.status == "ok" => (Color::Green, "ok".to_string()),
|
||||
Ok(resp) => {
|
||||
failures += 1;
|
||||
let msg = resp
|
||||
.error_message
|
||||
.unwrap_or_else(|| "unknown error".to_string());
|
||||
(&s.red, format!("error: {msg}"))
|
||||
(Color::Red, format!("error: {msg}"))
|
||||
}
|
||||
Err(e) => {
|
||||
failures += 1;
|
||||
(&s.red, format!("error: {e}"))
|
||||
(Color::Red, format!("error: {e}"))
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"{} {} {:>10} {:>7} / {:<7} {} {}",
|
||||
s.bold.apply_to(format!("{:<30}", info.id)),
|
||||
s.dim.apply_to(format!("{:<12}", info.provider)),
|
||||
format_context_window(info.limits.context_window),
|
||||
format_cost(info.costs.input_cost_per_mtok),
|
||||
format_cost(info.costs.output_cost_per_mtok),
|
||||
s.cyan
|
||||
.apply_to(format!("{:>10}", format_speed(info.estimated_output_tps))),
|
||||
status_color.apply_to(&status),
|
||||
let mut row = model_row(info, use_color);
|
||||
row.push(
|
||||
status
|
||||
.cell()
|
||||
.foreground_color(color_if(use_color, result_color)),
|
||||
);
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
let table = rows
|
||||
.table()
|
||||
.title(title)
|
||||
.border(Border::builder().build())
|
||||
.separator(Separator::builder().build());
|
||||
print_stdout(table)?;
|
||||
|
||||
if failures > 0 {
|
||||
bail!("{failures} model(s) failed");
|
||||
}
|
||||
|
|
@ -919,16 +957,14 @@ async fn test_models(provider: Option<&str>, model: Option<&str>, s: &Styles) ->
|
|||
bail!("No models found");
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
s.bold_dim.apply_to(format!(
|
||||
"{:<30} {:<12} {:>10} {:>7} {:>7} {:>10} RESULT",
|
||||
"MODEL", "PROVIDER", "CONTEXT", "COST", "", "SPEED",
|
||||
)),
|
||||
);
|
||||
let use_color = s.use_color;
|
||||
let mut title = models_title();
|
||||
title.push("RESULT".cell().bold(true));
|
||||
|
||||
let mut rows: Vec<Vec<CellStruct>> = Vec::new();
|
||||
let mut failures = 0u32;
|
||||
for info in &models_to_test {
|
||||
eprint!("Testing {}...", info.id);
|
||||
let params = GenerateParams::new(&info.id)
|
||||
.provider(&info.provider)
|
||||
.prompt("Say OK")
|
||||
|
|
@ -936,32 +972,36 @@ async fn test_models(provider: Option<&str>, model: Option<&str>, s: &Styles) ->
|
|||
|
||||
let result =
|
||||
tokio::time::timeout(Duration::from_secs(30), generate::generate(params)).await;
|
||||
eprintln!(" done");
|
||||
|
||||
let (status_color, status) = match result {
|
||||
Ok(Ok(_)) => (&s.green, "ok".to_string()),
|
||||
let (result_color, status) = match result {
|
||||
Ok(Ok(_)) => (Color::Green, "ok".to_string()),
|
||||
Ok(Err(e)) => {
|
||||
failures += 1;
|
||||
(&s.red, format!("error: {e}"))
|
||||
(Color::Red, format!("error: {e}"))
|
||||
}
|
||||
Err(_) => {
|
||||
failures += 1;
|
||||
(&s.red, "error: timeout (30s)".to_string())
|
||||
(Color::Red, "error: timeout (30s)".to_string())
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"{} {} {:>10} {:>7} / {:<7} {} {}",
|
||||
s.bold.apply_to(format!("{:<30}", info.id)),
|
||||
s.dim.apply_to(format!("{:<12}", info.provider)),
|
||||
format_context_window(info.limits.context_window),
|
||||
format_cost(info.costs.input_cost_per_mtok),
|
||||
format_cost(info.costs.output_cost_per_mtok),
|
||||
s.cyan
|
||||
.apply_to(format!("{:>10}", format_speed(info.estimated_output_tps))),
|
||||
status_color.apply_to(&status),
|
||||
let mut row = model_row(info, use_color);
|
||||
row.push(
|
||||
status
|
||||
.cell()
|
||||
.foreground_color(color_if(use_color, result_color)),
|
||||
);
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
let table = rows
|
||||
.table()
|
||||
.title(title)
|
||||
.border(Border::builder().build())
|
||||
.separator(Separator::builder().build());
|
||||
print_stdout(table)?;
|
||||
|
||||
if failures > 0 {
|
||||
bail!("{failures} model(s) failed");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ sha2 = { workspace = true }
|
|||
shlex = "1"
|
||||
strsim = "0.11"
|
||||
git2.workspace = true
|
||||
cli-table.workspace = true
|
||||
console.workspace = true
|
||||
indicatif.workspace = true
|
||||
tokio-util.workspace = true
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ use std::collections::HashMap;
|
|||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::Args;
|
||||
use cli_table::format::{Border, Separator};
|
||||
use cli_table::{print_stderr, Cell, CellStruct, Color, Style, Table};
|
||||
use fabro_git_storage::branchstore::{BranchStore, CommitInfo};
|
||||
use fabro_git_storage::gitobj::Store;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
|
@ -280,39 +282,52 @@ pub fn print_timeline(
|
|||
return;
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
" {} {} {}",
|
||||
styles.bold_dim.apply_to(format!("{:<6}", "@")),
|
||||
styles.bold_dim.apply_to(format!("{:<30}", "Node")),
|
||||
styles.bold_dim.apply_to("Details"),
|
||||
);
|
||||
let use_color = styles.use_color;
|
||||
let color_if = |color| if use_color { Some(color) } else { None };
|
||||
|
||||
for entry in timeline {
|
||||
let ordinal_str = format!("@{}", entry.ordinal);
|
||||
let mut details = Vec::new();
|
||||
if entry.visit > 1 {
|
||||
details.push(format!("visit {}, loop", entry.visit));
|
||||
}
|
||||
if parallel_map.contains_key(&entry.node_name) {
|
||||
details.push("parallel interior".to_string());
|
||||
}
|
||||
if entry.run_commit_sha.is_none() {
|
||||
details.push("no run commit".to_string());
|
||||
}
|
||||
let title = vec![
|
||||
"@".cell().bold(true),
|
||||
"Node".cell().bold(true),
|
||||
"Details".cell().bold(true),
|
||||
];
|
||||
|
||||
let detail_str = if details.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("({})", details.join(", "))
|
||||
};
|
||||
let rows: Vec<Vec<CellStruct>> = timeline
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let ordinal_str = format!("@{}", entry.ordinal);
|
||||
let mut details = Vec::new();
|
||||
if entry.visit > 1 {
|
||||
details.push(format!("visit {}, loop", entry.visit));
|
||||
}
|
||||
if parallel_map.contains_key(&entry.node_name) {
|
||||
details.push("parallel interior".to_string());
|
||||
}
|
||||
if entry.run_commit_sha.is_none() {
|
||||
details.push("no run commit".to_string());
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
" {} {:<30} {}",
|
||||
styles.cyan.apply_to(format!("{ordinal_str:<6}")),
|
||||
entry.node_name,
|
||||
styles.dim.apply_to(detail_str),
|
||||
);
|
||||
}
|
||||
let detail_str = if details.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("({})", details.join(", "))
|
||||
};
|
||||
|
||||
vec![
|
||||
ordinal_str.cell().foreground_color(color_if(Color::Cyan)),
|
||||
entry.node_name.clone().cell(),
|
||||
detail_str
|
||||
.cell()
|
||||
.foreground_color(color_if(Color::Ansi256(8))),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = rows
|
||||
.table()
|
||||
.title(title)
|
||||
.border(Border::builder().build())
|
||||
.separator(Separator::builder().build());
|
||||
let _ = print_stderr(table);
|
||||
}
|
||||
|
||||
/// Move both refs backward to the target checkpoint.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use std::path::{Path, PathBuf};
|
|||
use anyhow::{bail, Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::Args;
|
||||
use cli_table::format::{Border, Justify, Separator};
|
||||
use cli_table::{print_stdout, Cell, CellStruct, Color, Style, Table};
|
||||
use fabro_util::terminal::Styles;
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, info, warn};
|
||||
|
|
@ -408,20 +410,44 @@ fn short_run_id(id: &str) -> &str {
|
|||
}
|
||||
}
|
||||
|
||||
fn style_status(status: &RunStatus, styles: &Styles) -> String {
|
||||
fn color_if(use_color: bool, color: Color) -> Option<Color> {
|
||||
if use_color {
|
||||
Some(color)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn status_cell(status: &RunStatus, use_color: bool) -> CellStruct {
|
||||
let text = status.to_string();
|
||||
match status {
|
||||
RunStatus::Concluded(StageStatus::Success | StageStatus::PartialSuccess) => {
|
||||
format!("{}", styles.bold_green.apply_to(&text))
|
||||
}
|
||||
RunStatus::Concluded(StageStatus::Fail) => {
|
||||
format!("{}", styles.bold_red.apply_to(&text))
|
||||
}
|
||||
RunStatus::Running => format!("{}", styles.bold_cyan.apply_to(&text)),
|
||||
_ => format!("{}", styles.dim.apply_to(&text)),
|
||||
RunStatus::Concluded(StageStatus::Success | StageStatus::PartialSuccess) => text
|
||||
.cell()
|
||||
.bold(use_color)
|
||||
.foreground_color(color_if(use_color, Color::Green)),
|
||||
RunStatus::Concluded(StageStatus::Fail) => text
|
||||
.cell()
|
||||
.bold(use_color)
|
||||
.foreground_color(color_if(use_color, Color::Red)),
|
||||
RunStatus::Running => text
|
||||
.cell()
|
||||
.bold(use_color)
|
||||
.foreground_color(color_if(use_color, Color::Cyan)),
|
||||
_ => text
|
||||
.cell()
|
||||
.foreground_color(color_if(use_color, Color::Ansi256(8))),
|
||||
}
|
||||
}
|
||||
|
||||
fn abbreviate_home(path: &str) -> String {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
if let Ok(rel) = Path::new(path).strip_prefix(&home) {
|
||||
return format!("~/{}", rel.display());
|
||||
}
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
pub fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> {
|
||||
let base = default_runs_base();
|
||||
let runs = scan_runs(&base)?;
|
||||
|
|
@ -457,36 +483,55 @@ pub fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> {
|
|||
let mut display_runs = filtered;
|
||||
display_runs.reverse();
|
||||
|
||||
// Print table header
|
||||
let header = format!(
|
||||
"{:<17} {:<25} {:<17} {:<20} {:<10}",
|
||||
"RUN ID", "WORKFLOW", "STATUS", "DIRECTORY", "DURATION"
|
||||
);
|
||||
println!("{}", styles.bold.apply_to(&header));
|
||||
println!("{}", styles.dim.apply_to("-".repeat(header.len())));
|
||||
let use_color = styles.use_color;
|
||||
let title = vec![
|
||||
"RUN ID".cell().bold(true),
|
||||
"WORKFLOW".cell().bold(true),
|
||||
"STATUS".cell().bold(true),
|
||||
"DIRECTORY".cell().bold(true),
|
||||
"DURATION".cell().bold(true),
|
||||
];
|
||||
|
||||
for run in &display_runs {
|
||||
let run_id_display = short_run_id(&run.run_id);
|
||||
let duration_display = run
|
||||
.duration_ms
|
||||
.map(super::progress::format_duration_ms)
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let dir_display = run
|
||||
.host_repo_path
|
||||
.as_deref()
|
||||
.and_then(|p| Path::new(p).file_name())
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let status_display = style_status(&run.status, styles);
|
||||
println!(
|
||||
"{:<17} {:<25} {:<17} {:<20} {:<10}",
|
||||
styles.dim.apply_to(&run_id_display),
|
||||
run.workflow_name,
|
||||
status_display,
|
||||
dir_display,
|
||||
duration_display,
|
||||
);
|
||||
}
|
||||
let rows: Vec<Vec<CellStruct>> =
|
||||
display_runs
|
||||
.iter()
|
||||
.map(|run| {
|
||||
let run_id_display = short_run_id(&run.run_id);
|
||||
let duration_display = match run.duration_ms {
|
||||
Some(ms) => super::progress::format_duration_ms(ms),
|
||||
None => match run.start_time_dt {
|
||||
Some(start) => {
|
||||
let elapsed = Utc::now().signed_duration_since(start);
|
||||
super::progress::format_duration_ms(
|
||||
elapsed.num_milliseconds().max(0) as u64
|
||||
)
|
||||
}
|
||||
None => "-".to_string(),
|
||||
},
|
||||
};
|
||||
let dir_display = run
|
||||
.host_repo_path
|
||||
.as_deref()
|
||||
.map(abbreviate_home)
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
vec![
|
||||
run_id_display
|
||||
.cell()
|
||||
.foreground_color(color_if(use_color, Color::Ansi256(8))),
|
||||
run.workflow_name.clone().cell(),
|
||||
status_cell(&run.status, use_color),
|
||||
dir_display.cell(),
|
||||
duration_display.cell(),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = rows
|
||||
.table()
|
||||
.title(title)
|
||||
.border(Border::builder().build())
|
||||
.separator(Separator::builder().build());
|
||||
print_stdout(table)?;
|
||||
|
||||
eprintln!("\n{} run(s) listed.", display_runs.len());
|
||||
Ok(())
|
||||
|
|
@ -614,34 +659,48 @@ pub fn df_from(args: &DfArgs, data_dir: &Path, runs_base: &Path, logs_base: &Pat
|
|||
};
|
||||
let log_reclaim_pct = if total_log_size > 0 { 100 } else { 0 };
|
||||
|
||||
println!(
|
||||
"{:<14}{:>5}{:>11}{:>12}{:>16}",
|
||||
"TYPE", "COUNT", "ACTIVE", "SIZE", "RECLAIMABLE"
|
||||
);
|
||||
println!(
|
||||
"{:<14}{:>5}{:>11}{:>12}{:>12} ({run_reclaim_pct}%)",
|
||||
"Runs",
|
||||
runs.len(),
|
||||
active_count,
|
||||
format_size(total_run_size),
|
||||
format_size(reclaimable_run_size),
|
||||
);
|
||||
println!(
|
||||
"{:<14}{:>5}{:>11}{:>12}{:>12} ({log_reclaim_pct}%)",
|
||||
"Logs",
|
||||
log_count,
|
||||
"-",
|
||||
format_size(total_log_size),
|
||||
format_size(total_log_size),
|
||||
);
|
||||
println!(
|
||||
"{:<14}{:>5}{:>11}{:>12}{:>12} (0%)",
|
||||
"Databases",
|
||||
db_count,
|
||||
"-",
|
||||
format_size(total_db_size),
|
||||
format_size(0),
|
||||
);
|
||||
let df_title = vec![
|
||||
"TYPE".cell().bold(true),
|
||||
"COUNT".cell().bold(true).justify(Justify::Right),
|
||||
"ACTIVE".cell().bold(true).justify(Justify::Right),
|
||||
"SIZE".cell().bold(true).justify(Justify::Right),
|
||||
"RECLAIMABLE".cell().bold(true).justify(Justify::Right),
|
||||
];
|
||||
let df_rows: Vec<Vec<CellStruct>> = vec![
|
||||
vec![
|
||||
"Runs".cell(),
|
||||
runs.len().cell().justify(Justify::Right),
|
||||
active_count.cell().justify(Justify::Right),
|
||||
format_size(total_run_size).cell().justify(Justify::Right),
|
||||
format!("{} ({run_reclaim_pct}%)", format_size(reclaimable_run_size))
|
||||
.cell()
|
||||
.justify(Justify::Right),
|
||||
],
|
||||
vec![
|
||||
"Logs".cell(),
|
||||
log_count.cell().justify(Justify::Right),
|
||||
"-".cell().justify(Justify::Right),
|
||||
format_size(total_log_size).cell().justify(Justify::Right),
|
||||
format!("{} ({log_reclaim_pct}%)", format_size(total_log_size))
|
||||
.cell()
|
||||
.justify(Justify::Right),
|
||||
],
|
||||
vec![
|
||||
"Databases".cell(),
|
||||
db_count.cell().justify(Justify::Right),
|
||||
"-".cell().justify(Justify::Right),
|
||||
format_size(total_db_size).cell().justify(Justify::Right),
|
||||
format!("{} (0%)", format_size(0))
|
||||
.cell()
|
||||
.justify(Justify::Right),
|
||||
],
|
||||
];
|
||||
let df_table = df_rows
|
||||
.table()
|
||||
.title(df_title)
|
||||
.border(Border::builder().build())
|
||||
.separator(Separator::builder().build());
|
||||
print_stdout(df_table)?;
|
||||
|
||||
println!();
|
||||
println!("Data directory: {}", data_dir.display());
|
||||
|
|
@ -649,46 +708,57 @@ pub fn df_from(args: &DfArgs, data_dir: &Path, runs_base: &Path, logs_base: &Pat
|
|||
// --- Verbose per-run breakdown ---
|
||||
if args.verbose {
|
||||
println!();
|
||||
println!(
|
||||
"{:<30} {:<18} {:<10} {:>5} {:>12}",
|
||||
"RUN ID", "WORKFLOW", "STATUS", "AGE", "SIZE"
|
||||
);
|
||||
let verbose_title = vec![
|
||||
"RUN ID".cell().bold(true),
|
||||
"WORKFLOW".cell().bold(true),
|
||||
"STATUS".cell().bold(true),
|
||||
"AGE".cell().bold(true).justify(Justify::Right),
|
||||
"SIZE".cell().bold(true).justify(Justify::Right),
|
||||
];
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
for detail in &run_details {
|
||||
let run_id_display = short_run_id(&detail.run_id);
|
||||
let workflow_display = if detail.workflow_name.len() > 16 {
|
||||
format!("{}...", &detail.workflow_name[..13])
|
||||
} else {
|
||||
detail.workflow_name.clone()
|
||||
};
|
||||
let age = if let Some(dt) = detail.start_time_dt {
|
||||
let dur = now.signed_duration_since(dt);
|
||||
if dur.num_days() > 0 {
|
||||
format!("{}d", dur.num_days())
|
||||
} else if dur.num_hours() > 0 {
|
||||
format!("{}h", dur.num_hours())
|
||||
let verbose_rows: Vec<Vec<CellStruct>> = run_details
|
||||
.iter()
|
||||
.map(|detail| {
|
||||
let run_id_display = short_run_id(&detail.run_id);
|
||||
let workflow_display = if detail.workflow_name.len() > 16 {
|
||||
format!("{}...", &detail.workflow_name[..13])
|
||||
} else {
|
||||
format!("{}m", dur.num_minutes().max(1))
|
||||
}
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
let reclaimable_marker = if !detail.status.is_running() {
|
||||
" *"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(
|
||||
"{:<30} {:<18} {:<10} {:>5} {:>10}{}",
|
||||
run_id_display,
|
||||
workflow_display,
|
||||
detail.status,
|
||||
age,
|
||||
format_size(detail.size),
|
||||
reclaimable_marker,
|
||||
);
|
||||
}
|
||||
detail.workflow_name.clone()
|
||||
};
|
||||
let age = if let Some(dt) = detail.start_time_dt {
|
||||
let dur = now.signed_duration_since(dt);
|
||||
if dur.num_days() > 0 {
|
||||
format!("{}d", dur.num_days())
|
||||
} else if dur.num_hours() > 0 {
|
||||
format!("{}h", dur.num_hours())
|
||||
} else {
|
||||
format!("{}m", dur.num_minutes().max(1))
|
||||
}
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
let size_display = if !detail.status.is_running() {
|
||||
format!("{} *", format_size(detail.size))
|
||||
} else {
|
||||
format_size(detail.size)
|
||||
};
|
||||
vec![
|
||||
run_id_display.cell(),
|
||||
workflow_display.cell(),
|
||||
detail.status.to_string().cell(),
|
||||
age.cell().justify(Justify::Right),
|
||||
size_display.cell().justify(Justify::Right),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let verbose_table = verbose_rows
|
||||
.table()
|
||||
.title(verbose_title)
|
||||
.border(Border::builder().build())
|
||||
.separator(Separator::builder().build());
|
||||
print_stdout(verbose_table)?;
|
||||
println!();
|
||||
println!("* = reclaimable");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue