feat: added list and clean cli cmd

This commit is contained in:
robcholz
2026-02-07 16:20:51 -05:00
parent b3ac485ccf
commit c98bc78f55
7 changed files with 594 additions and 263 deletions
Generated
+169 -244
View File
@@ -32,6 +32,56 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "backtrace"
version = "0.3.76"
@@ -68,12 +118,6 @@ version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cassowary"
version = "0.3.0"
@@ -95,6 +139,46 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.5.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "color-eyre"
version = "0.6.5"
@@ -122,6 +206,12 @@ dependencies = [
"tracing-error",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "compact_str"
version = "0.8.1"
@@ -136,6 +226,19 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "console"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.0",
"windows-sys 0.61.2",
]
[[package]]
name = "crossterm"
version = "0.28.1"
@@ -206,6 +309,18 @@ dependencies = [
"serde_core",
]
[[package]]
name = "dialoguer"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96"
dependencies = [
"console",
"shell-words",
"tempfile",
"zeroize",
]
[[package]]
name = "dispatch2"
version = "0.3.0"
@@ -224,6 +339,12 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -262,95 +383,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -436,6 +474,12 @@ dependencies = [
"syn",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.13.0"
@@ -467,12 +511,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lexopt"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa0e2a1fcbe2f6be6c42e342259976206b383122fc152e872795338b5a3f3a7"
[[package]]
name = "libc"
version = "0.2.180"
@@ -557,7 +595,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -646,6 +684,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "owo-colors"
version = "4.2.3"
@@ -687,12 +731,6 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -868,6 +906,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shell-words"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
[[package]]
name = "signal-hook"
version = "0.3.18"
@@ -899,28 +943,12 @@ dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -1039,34 +1067,6 @@ dependencies = [
"time-core",
]
[[package]]
name = "tokio"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.9.11+spec-1.1.0"
@@ -1177,16 +1177,6 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "tui-textarea"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae"
dependencies = [
"ratatui",
"unicode-width 0.2.0",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
@@ -1222,6 +1212,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.20.0"
@@ -1245,11 +1241,11 @@ name = "vibebox"
version = "0.1.0"
dependencies = [
"block2",
"clap",
"color-eyre",
"crossterm",
"dialoguer",
"dispatch2",
"futures",
"lexopt",
"libc",
"objc2",
"objc2-foundation",
@@ -1259,12 +1255,9 @@ dependencies = [
"tempfile",
"thiserror",
"time",
"tokio",
"toml",
"tracing",
"tracing-subscriber",
"tui-textarea",
"unicode-width 0.2.0",
"uuid",
]
@@ -1362,16 +1355,7 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
"windows-targets",
]
[[package]]
@@ -1389,31 +1373,14 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
@@ -1422,96 +1389,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
@@ -1523,3 +1442,9 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+2 -5
View File
@@ -20,7 +20,7 @@ objc2-virtualization = "*"
block2 = "*"
dispatch2 = "*"
libc = "*"
lexopt = "0.3"
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
tempfile = "3"
thiserror = "2.0.18"
@@ -29,10 +29,7 @@ toml = "0.9.8"
uuid = { version = "1", features = ["v7", "serde"] }
color-eyre = "0.6.3"
crossterm = { version = "0.28.1", features = ["event-stream"] }
futures = "0.3.31"
ratatui = { version = "0.29.0", features = ["unstable-rendered-line-info"] }
tokio = { version = "1.40.0", features = ["full"] }
tui-textarea = { version = "0.7.0", default-features = false, features = ["ratatui"] }
unicode-width = "0.2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
dialoguer = "0.12.0"
+155 -6
View File
@@ -2,24 +2,52 @@ use std::{
env,
ffi::OsString,
io::{self, IsTerminal, Write},
path::PathBuf,
sync::{Arc, Mutex},
};
use clap::Parser;
use color_eyre::Result;
use dialoguer::Confirm;
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use tracing_subscriber::EnvFilter;
use vibebox::tui::{AppState, VmInfo};
use vibebox::{SessionManager, commands, config, instance, tui, vm, vm_manager};
use vibebox::{SessionManager, commands, config, instance, session_manager, tui, vm, vm_manager};
#[derive(Debug, Parser)]
#[command(name = "vibebox", version, about = "Vibebox CLI")]
struct Cli {
/// Path to vibebox.toml (relative to the current directory)
#[arg(short = 'c', long = "config", value_name = "PATH", global = true)]
config: Option<PathBuf>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, clap::Subcommand)]
enum Command {
/// List all sessions
List,
/// Delete the current project's .vibebox directory
Clean,
}
fn main() -> Result<()> {
init_tracing();
color_eyre::install()?;
let raw_args: Vec<OsString> = env::args_os().collect();
let cli = Cli::parse();
let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
let config = config::load_config(&cwd);
if let Some(command) = cli.command {
return handle_command(command, &cwd);
}
let config_override = cli.config.clone();
let raw_args: Vec<OsString> = env::args_os().collect();
let config = config::load_config_with_path(&cwd, config_override.as_deref());
if env::var("VIBEBOX_VM_MANAGER").as_deref() == Ok("1") {
tracing::info!("starting vm manager mode");
@@ -74,14 +102,135 @@ fn main() -> Result<()> {
}
tracing::info!(auto_shutdown_ms, "auto shutdown config");
let manager_conn = vm_manager::ensure_manager(&raw_args, auto_shutdown_ms)
.map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
let manager_conn =
vm_manager::ensure_manager(&raw_args, auto_shutdown_ms, config_override.as_deref())
.map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
instance::run_with_ssh(manager_conn).map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
Ok(())
}
fn handle_command(command: Command, cwd: &PathBuf) -> Result<()> {
match command {
Command::List => {
let manager = SessionManager::new()?;
let sessions = manager.list_sessions()?;
if sessions.is_empty() {
println!("No sessions were found.");
return Ok(());
}
let rows: Vec<tui::SessionListRow> = sessions
.into_iter()
.map(|session| tui::SessionListRow {
name: project_name(&session.directory),
id: session.id,
directory: relative_to_home(&session.directory),
last_active: format_last_active(session.last_active.as_deref()),
active: if session.active {
"yes".to_string()
} else {
"no".to_string()
},
})
.collect();
tui::render_sessions_table(&rows)?;
Ok(())
}
Command::Clean => {
let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME);
if !instance_dir.exists() {
println!("No .vibebox directory found at {}", instance_dir.display());
return Ok(());
}
let confirmed = Confirm::new()
.with_prompt(format!(
"Delete {} and all its contents?",
instance_dir.display()
))
.default(false)
.interact()?;
if !confirmed {
println!("Cancelled.");
return Ok(());
}
let manager = SessionManager::new()?;
let summary = manager.clean_project(cwd)?;
println!(
"Deleted {} (removed={}, session_records_removed={})",
summary.instance_dir.display(),
summary.removed_instance_dir,
summary.removed_sessions
);
Ok(())
}
}
}
fn project_name(directory: &PathBuf) -> String {
directory
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("-")
.to_string()
}
fn relative_to_home(directory: &PathBuf) -> String {
let Ok(home) = env::var("HOME") else {
return directory.display().to_string();
};
let home_path = PathBuf::from(home);
if let Ok(stripped) = directory.strip_prefix(&home_path) {
if stripped.components().next().is_none() {
return "~".to_string();
}
return format!("~/{}", stripped.display());
}
directory.display().to_string()
}
fn format_last_active(value: Option<&str>) -> String {
let Some(raw) = value else {
return "-".to_string();
};
let parsed = OffsetDateTime::parse(raw, &Rfc3339);
let Ok(timestamp) = parsed else {
return raw.to_string();
};
let now = OffsetDateTime::now_utc();
let mut seconds = (now - timestamp).whole_seconds();
if seconds < 0 {
seconds = 0;
}
let seconds = seconds as i64;
let week_seconds = 7 * 24 * 60 * 60;
if seconds >= week_seconds {
let formatted =
match time::format_description::parse("[year]-[month]-[day] [hour]:[minute]Z") {
Ok(format) => timestamp
.format(&format)
.unwrap_or_else(|_| raw.to_string()),
Err(_) => timestamp
.format(&Rfc3339)
.unwrap_or_else(|_| raw.to_string()),
};
return formatted;
}
if seconds < 60 {
return "just now".to_string();
}
if seconds < 60 * 60 {
let mins = seconds / 60;
return format!("{} min{} ago", mins, if mins == 1 { "" } else { "s" });
}
if seconds < 60 * 60 * 24 {
let hours = seconds / (60 * 60);
return format!("{} hour{} ago", hours, if hours == 1 { "" } else { "s" });
}
let days = seconds / (60 * 60 * 24);
format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
}
fn init_tracing() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
+60 -4
View File
@@ -1,5 +1,5 @@
use std::{
fs, io,
env, fs, io,
path::{Path, PathBuf},
};
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::vm::DirectoryShare;
pub const CONFIG_FILENAME: &str = "vibebox.toml";
pub const CONFIG_PATH_ENV: &str = "VIBEBOX_CONFIG_PATH";
const DEFAULT_CPU_COUNT: usize = 2;
const DEFAULT_RAM_MB: u64 = 2048;
@@ -93,8 +94,11 @@ pub fn config_path(project_root: &Path) -> PathBuf {
project_root.join(CONFIG_FILENAME)
}
pub fn ensure_config_file(project_root: &Path) -> Result<PathBuf, io::Error> {
let path = config_path(project_root);
pub fn ensure_config_file(
project_root: &Path,
override_path: Option<&Path>,
) -> Result<PathBuf, io::Error> {
let path = resolve_config_path(project_root, override_path);
if !path.exists() {
let default_config = Config::default();
let contents = toml::to_string_pretty(&default_config).unwrap_or_default();
@@ -105,7 +109,11 @@ pub fn ensure_config_file(project_root: &Path) -> Result<PathBuf, io::Error> {
}
pub fn load_config(project_root: &Path) -> Config {
let path = match ensure_config_file(project_root) {
load_config_with_path(project_root, None)
}
pub fn load_config_with_path(project_root: &Path, override_path: Option<&Path>) -> Config {
let path = match ensure_config_file(project_root, override_path) {
Ok(path) => path,
Err(err) => die(&format!("failed to create config: {err}")),
};
@@ -144,6 +152,54 @@ pub fn load_config(project_root: &Path) -> Config {
config
}
fn resolve_config_path(project_root: &Path, override_path: Option<&Path>) -> PathBuf {
let root = match fs::canonicalize(project_root) {
Ok(root) => root,
Err(err) => die(&format!("failed to resolve project root: {err}")),
};
let override_path = override_path
.map(PathBuf::from)
.or_else(|| env::var_os(CONFIG_PATH_ENV).map(PathBuf::from));
let raw_path = if let Some(path) = override_path {
if path.is_absolute() {
path
} else {
project_root.join(path)
}
} else {
config_path(project_root)
};
let normalized = normalize_path(&raw_path);
if !normalized.starts_with(&root) {
die(&format!(
"config path must be within {}: {}",
root.display(),
normalized.display()
));
}
normalized
}
fn normalize_path(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
std::path::Component::RootDir => {
normalized.push(std::path::MAIN_SEPARATOR.to_string());
}
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
let _ = normalized.pop();
}
std::path::Component::Normal(part) => normalized.push(part),
}
}
normalized
}
fn validate_schema(value: &toml::Value) -> Vec<String> {
let mut errors = Vec::new();
let root = match value.as_table() {
+96
View File
@@ -1,6 +1,7 @@
use std::{
env, fs,
io::{self, Write},
os::unix::fs::FileTypeExt,
path::{Path, PathBuf},
};
@@ -13,6 +14,8 @@ pub const GLOBAL_CACHE_DIR_NAME: &str = "vibebox";
pub const GLOBAL_DIR_NAME: &str = ".vibebox";
pub const INSTANCE_FILENAME: &str = "instance.toml";
pub const SESSION_TOML_SUFFIX: &str = ".toml";
pub const VM_MANAGER_SOCKET_NAME: &str = "vm.sock";
pub const VM_MANAGER_PID_NAME: &str = "vm.pid";
const SESSIONS_DIR_NAME: &str = "sessions";
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -20,6 +23,7 @@ pub struct SessionRecord {
pub directory: PathBuf,
pub id: String,
pub last_active: Option<String>,
pub active: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -41,6 +45,13 @@ pub struct SessionManager {
sessions_dir: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CleanSummary {
pub instance_dir: PathBuf,
pub removed_instance_dir: bool,
pub removed_sessions: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error("HOME environment variable is not set")]
@@ -138,15 +149,33 @@ impl SessionManager {
let mut records = Vec::with_capacity(sessions.len());
for session in sessions {
let meta = read_instance_metadata(&session.directory)?;
let active = is_session_active(&session.directory);
records.push(SessionRecord {
directory: session.directory,
id: session.id,
last_active: meta.last_active,
active,
});
}
Ok(records)
}
pub fn clean_project(&self, directory: &Path) -> Result<CleanSummary, SessionError> {
let directory = self.normalize_directory(directory)?;
let instance_dir = directory.join(INSTANCE_DIR_NAME);
let mut removed_instance_dir = false;
if instance_dir.exists() {
fs::remove_dir_all(&instance_dir)?;
removed_instance_dir = true;
}
let removed_sessions = self.remove_session_records_for_directory(&directory)?;
Ok(CleanSummary {
instance_dir,
removed_instance_dir,
removed_sessions,
})
}
fn normalize_directory(&self, directory: &Path) -> Result<PathBuf, SessionError> {
if !directory.is_absolute() {
return Err(SessionError::NonAbsoluteDirectory(directory.to_path_buf()));
@@ -206,6 +235,36 @@ impl SessionManager {
Ok((sessions, removed))
}
fn remove_session_records_for_directory(
&self,
directory: &Path,
) -> Result<usize, SessionError> {
if !self.sessions_dir.exists() {
return Ok(0);
}
let mut removed = 0usize;
for entry in fs::read_dir(&self.sessions_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let record = read_session_file(&path)?;
if record.directory == directory {
fs::remove_file(&path)?;
removed += 1;
}
}
if removed > 0 {
tracing::info!(
directory = %directory.display(),
removed,
"removed session records"
);
}
Ok(removed)
}
}
fn is_vibebox_dir(directory: &Path) -> bool {
@@ -215,6 +274,43 @@ fn is_vibebox_dir(directory: &Path) -> bool {
directory.join(CONFIG_FILENAME).is_file()
}
fn is_session_active(directory: &Path) -> bool {
let instance_dir = directory.join(INSTANCE_DIR_NAME);
let pid_path = instance_dir.join(VM_MANAGER_PID_NAME);
let socket_path = instance_dir.join(VM_MANAGER_SOCKET_NAME);
let pid = read_pid(&pid_path);
let is_alive = pid.map(pid_is_alive).unwrap_or(false);
if !is_alive {
let _ = fs::remove_file(&pid_path);
return false;
}
if let Ok(metadata) = fs::metadata(&socket_path) {
return metadata.file_type().is_socket();
}
true
}
fn read_pid(path: &Path) -> Option<u32> {
let content = fs::read_to_string(path).ok()?;
content.trim().parse::<u32>().ok()
}
fn pid_is_alive(pid: u32) -> bool {
let pid = pid as libc::pid_t;
let result = unsafe { libc::kill(pid, 0) };
if result == 0 {
return true;
}
match std::io::Error::last_os_error().raw_os_error() {
Some(code) if code == libc::EPERM => true,
Some(code) if code == libc::ESRCH => false,
_ => false,
}
}
fn read_session_file(path: &Path) -> Result<SessionEntry, SessionError> {
let raw = fs::read_to_string(path)?;
let record: SessionEntry = toml::from_str(&raw)?;
+62 -1
View File
@@ -20,7 +20,7 @@ use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, List, ListItem, Paragraph, Widget},
widgets::{Block, Borders, Cell, List, ListItem, Paragraph, Row, Table, Widget},
};
use crate::vm;
@@ -98,6 +98,15 @@ pub struct VibeboxCommand {
pub description: String,
}
#[derive(Debug, Clone)]
pub struct SessionListRow {
pub name: String,
pub directory: String,
pub last_active: String,
pub active: String,
pub id: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct PageLayout {
header: Rect,
@@ -174,6 +183,58 @@ pub fn render_commands_component(app: &mut AppState) -> Result<()> {
Ok(())
}
pub fn render_sessions_table(rows: &[SessionListRow]) -> Result<()> {
let (width, _) = crossterm::terminal::size()?;
if width == 0 {
return Ok(());
}
let height = (rows.len() as u16).saturating_add(3);
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
let area = Rect::new(0, 0, width, height);
let header = Row::new(vec![
Cell::from("Name"),
Cell::from("Last Active"),
Cell::from("Active"),
Cell::from("ID"),
Cell::from("Directory"),
])
.style(Style::default().fg(Color::Cyan));
let table_rows = rows.iter().map(|row| {
Row::new(vec![
Cell::from(row.name.clone()),
Cell::from(row.last_active.clone()),
Cell::from(row.active.clone()),
Cell::from(row.id.clone()),
Cell::from(row.directory.clone()),
])
});
let table = Table::new(
table_rows,
[
Constraint::Length(16),
Constraint::Length(14),
Constraint::Length(8),
Constraint::Length(36),
Constraint::Min(24),
],
)
.header(header)
.block(Block::default().title("Sessions").borders(Borders::ALL))
.column_spacing(2);
table.render(area, &mut buffer);
let mut stdout = io::stdout();
execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?;
write_buffer_with_style(&buffer, &mut stdout)?;
stdout.flush()?;
Ok(())
}
pub fn passthrough_vm_io(
app: Arc<Mutex<AppState>>,
output_monitor: Arc<vm::OutputMonitor>,
+50 -3
View File
@@ -15,26 +15,30 @@ use std::{
};
use crate::{
config::CONFIG_PATH_ENV,
instance::SERIAL_LOG_NAME,
instance::{
InstanceConfig, build_ssh_login_actions, ensure_instance_dir, ensure_ssh_keypair,
extract_ipv4, load_or_create_instance_config, write_instance_config,
},
session_manager::{GLOBAL_DIR_NAME, INSTANCE_FILENAME},
session_manager::{
GLOBAL_DIR_NAME, INSTANCE_FILENAME, VM_MANAGER_PID_NAME, VM_MANAGER_SOCKET_NAME,
},
vm::{self, DirectoryShare, LoginAction, VmInput},
};
const VM_MANAGER_SOCKET_NAME: &str = "vm.sock";
const VM_MANAGER_LOCK_NAME: &str = "vm.lock";
const VM_MANAGER_LOG_NAME: &str = "vm_manager.log";
pub fn ensure_manager(
raw_args: &[std::ffi::OsString],
auto_shutdown_ms: u64,
config_path: Option<&Path>,
) -> Result<UnixStream, Box<dyn std::error::Error>> {
let project_root = env::current_dir()?;
tracing::debug!(root = %project_root.display(), "ensure vm manager");
let instance_dir = ensure_instance_dir(&project_root)?;
cleanup_stale_manager(&instance_dir);
let socket_path = instance_dir.join(VM_MANAGER_SOCKET_NAME);
if let Ok(stream) = UnixStream::connect(&socket_path) {
@@ -47,7 +51,7 @@ pub fn ensure_manager(
let mut lock_file = acquire_spawn_lock(&lock_path)?;
if lock_file.is_some() {
tracing::info!(path = %socket_path.display(), "spawning vm manager");
spawn_manager_process(raw_args, auto_shutdown_ms, &instance_dir)?;
spawn_manager_process(raw_args, auto_shutdown_ms, &instance_dir, config_path)?;
} else {
tracing::info!(
path = %socket_path.display(),
@@ -96,6 +100,7 @@ pub fn run_manager(
) -> Result<(), Box<dyn std::error::Error>> {
let project_root = env::current_dir()?;
tracing::info!(root = %project_root.display(), "vm manager starting");
let _pid_guard = ensure_pid_file(&project_root)?;
run_manager_with(
&project_root,
args,
@@ -113,6 +118,7 @@ fn spawn_manager_process(
raw_args: &[std::ffi::OsString],
auto_shutdown_ms: u64,
instance_dir: &Path,
config_path: Option<&Path>,
) -> Result<(), Box<dyn std::error::Error>> {
let exe = env::current_exe()?;
let mut supervisor_exe = exe.clone();
@@ -133,6 +139,9 @@ fn spawn_manager_process(
}
cmd.env("VIBEBOX_LOG_NO_COLOR", "1");
cmd.env("VIBEBOX_AUTO_SHUTDOWN_MS", auto_shutdown_ms.to_string());
if let Some(path) = config_path {
cmd.env(CONFIG_PATH_ENV, path);
}
tracing::info!(auto_shutdown_ms, "vm manager process spawn requested");
let log_path = instance_dir.join(VM_MANAGER_LOG_NAME);
let log_file = fs::OpenOptions::new()
@@ -155,6 +164,44 @@ fn spawn_manager_process(
Ok(())
}
fn ensure_pid_file(project_root: &Path) -> Result<PidFileGuard, Box<dyn std::error::Error>> {
let instance_dir = ensure_instance_dir(project_root)?;
let pid_path = instance_dir.join(VM_MANAGER_PID_NAME);
if let Ok(content) = fs::read_to_string(&pid_path) {
if let Ok(pid) = content.trim().parse::<u32>() {
if pid_is_alive(pid) {
return Err(format!("vm manager already running (pid {pid})").into());
}
}
let _ = fs::remove_file(&pid_path);
}
fs::write(&pid_path, format!("{}\n", std::process::id()))?;
let _ = fs::set_permissions(&pid_path, fs::Permissions::from_mode(0o600));
Ok(PidFileGuard { path: pid_path })
}
fn cleanup_stale_manager(instance_dir: &Path) {
let pid_path = instance_dir.join(VM_MANAGER_PID_NAME);
if let Ok(content) = fs::read_to_string(&pid_path) {
if let Ok(pid) = content.trim().parse::<u32>() {
if pid_is_alive(pid) {
return;
}
}
}
let _ = fs::remove_file(&pid_path);
}
struct PidFileGuard {
path: PathBuf,
}
impl Drop for PidFileGuard {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
fn detach_from_terminal() {
unsafe {
libc::setsid();