mirror of
https://github.com/robcholz/vibebox.git
synced 2026-05-20 06:54:40 +02:00
Generated
+169
-244
@@ -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
@@ -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"
|
||||
|
||||
+6
-2
@@ -42,8 +42,12 @@
|
||||
5. [x] wire up SessionManager.
|
||||
6. [x] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself).
|
||||
7. [x] setup vibebox commands
|
||||
8. [ ] setup cli commands.
|
||||
9. [ ] fix ui overlap.
|
||||
8. [x] setup cli commands.
|
||||
1. [x] Organize all the params.
|
||||
2. [x] Remove old cli.
|
||||
3. [x] add an actual config file.
|
||||
4. [x] set up the cli.
|
||||
9. [ ] fix ui overlap, and consistency issue.
|
||||
10. [ ] intensive integration test.
|
||||
|
||||
## Publish
|
||||
|
||||
+300
-29
@@ -2,30 +2,64 @@ use std::{
|
||||
env,
|
||||
ffi::OsString,
|
||||
io::{self, IsTerminal, Write},
|
||||
path::{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};
|
||||
|
||||
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 30000;
|
||||
#[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,
|
||||
/// Explain mounts and mappings
|
||||
Explain,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_tracing();
|
||||
color_eyre::install()?;
|
||||
|
||||
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");
|
||||
if let Some(command) = cli.command {
|
||||
return handle_command(command, &cwd, cli.config.as_deref());
|
||||
}
|
||||
|
||||
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");
|
||||
let args = vm::parse_cli().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
|
||||
let args = vm::VmArg {
|
||||
cpu_count: config.box_cfg.cpu_count,
|
||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||
no_default_mounts: false,
|
||||
mounts: config.box_cfg.mounts.clone(),
|
||||
};
|
||||
let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
|
||||
tracing::info!(auto_shutdown_ms, "vm manager config");
|
||||
if let Err(err) = vm_manager::run_manager(args, auto_shutdown_ms) {
|
||||
tracing::error!(error = %err, "vm manager exited");
|
||||
@@ -34,33 +68,24 @@ fn main() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let args = vm::parse_cli().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
tracing::debug!("parsed cli args");
|
||||
if args.version() {
|
||||
vm::print_version();
|
||||
return Ok(());
|
||||
}
|
||||
if args.help() {
|
||||
vm::print_help();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
vm::ensure_signed();
|
||||
|
||||
let vm_args = vm::VmArg {
|
||||
cpu_count: config.box_cfg.cpu_count,
|
||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||
no_default_mounts: false,
|
||||
mounts: config.box_cfg.mounts.clone(),
|
||||
};
|
||||
|
||||
let vm_info = VmInfo {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
max_memory_mb: args.ram_mb(),
|
||||
cpu_cores: args.cpu_count(),
|
||||
max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
|
||||
cpu_cores: vm_args.cpu_count,
|
||||
};
|
||||
let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
|
||||
let auto_shutdown_ms = config::load_config(&cwd)
|
||||
.map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?
|
||||
.auto_shutdown_ms
|
||||
.unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
|
||||
let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
|
||||
if let Ok(manager) = SessionManager::new() {
|
||||
if let Err(err) = manager.update_global_sessions(&cwd) {
|
||||
tracing::warn!(error = %err, "failed to update global session list");
|
||||
tracing::warn!(error = %err, "failed to update a global session list");
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("failed to initialize session manager");
|
||||
@@ -79,14 +104,260 @@ 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, config_override: Option<&Path>) -> 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(())
|
||||
}
|
||||
Command::Explain => {
|
||||
let config = config::load_config_with_path(cwd, config_override);
|
||||
let mounts = build_mount_rows(cwd, &config)?;
|
||||
let networks = build_network_rows(cwd)?;
|
||||
if mounts.is_empty() && networks.is_empty() {
|
||||
println!("No mounts or network info available.");
|
||||
return Ok(());
|
||||
}
|
||||
tui::render_explain_tables(&mounts, &networks)?;
|
||||
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 build_mount_rows(cwd: &Path, config: &config::Config) -> Result<Vec<tui::MountListRow>> {
|
||||
let mut rows = Vec::new();
|
||||
rows.extend(default_mounts(cwd)?);
|
||||
for spec in &config.box_cfg.mounts {
|
||||
rows.push(parse_mount_spec(cwd, spec, false)?);
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn build_network_rows(cwd: &Path) -> Result<Vec<tui::NetworkListRow>> {
|
||||
let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME);
|
||||
let mut vm_ip = "-".to_string();
|
||||
if let Ok(Some(ip)) = instance::read_instance_vm_ip(&instance_dir) {
|
||||
vm_ip = ip;
|
||||
}
|
||||
let host_to_vm = if vm_ip == "-" {
|
||||
"ssh: <pending>:22".to_string()
|
||||
} else {
|
||||
format!("ssh: {vm_ip}:22")
|
||||
};
|
||||
let row = tui::NetworkListRow {
|
||||
network_type: "NAT".to_string(),
|
||||
vm_ip: vm_ip.clone(),
|
||||
host_to_vm,
|
||||
vm_to_host: "none".to_string(),
|
||||
};
|
||||
Ok(vec![row])
|
||||
}
|
||||
|
||||
fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>> {
|
||||
let project_name = cwd
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("project");
|
||||
let project_guest = format!("/root/{project_name}");
|
||||
let project_host = relative_to_home(&cwd.to_path_buf());
|
||||
let mut rows = vec![tui::MountListRow {
|
||||
host: project_host,
|
||||
guest: project_guest,
|
||||
mode: "read-write".to_string(),
|
||||
default_mount: "yes".to_string(),
|
||||
}];
|
||||
|
||||
let home = env::var("HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from("/"));
|
||||
let cache_home = env::var("XDG_CACHE_HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| home.join(".cache"));
|
||||
let cache_dir = cache_home.join(session_manager::GLOBAL_CACHE_DIR_NAME);
|
||||
let guest_mise_cache = cache_dir.join(".guest-mise-cache");
|
||||
rows.push(tui::MountListRow {
|
||||
host: relative_to_home(&guest_mise_cache),
|
||||
guest: "/root/.local/share/mise".to_string(),
|
||||
mode: "read-write".to_string(),
|
||||
default_mount: "yes".to_string(),
|
||||
});
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn parse_mount_spec(cwd: &Path, spec: &str, default_mount: bool) -> Result<tui::MountListRow> {
|
||||
let parts: Vec<&str> = spec.split(':').collect();
|
||||
if parts.len() < 2 || parts.len() > 3 {
|
||||
return Err(color_eyre::eyre::eyre!("invalid mount spec: {spec}"));
|
||||
}
|
||||
let host_part = parts[0];
|
||||
let guest_part = parts[1];
|
||||
let mode = if parts.len() == 3 {
|
||||
match parts[2] {
|
||||
"read-only" => "read-only",
|
||||
"read-write" => "read-write",
|
||||
other => {
|
||||
return Err(color_eyre::eyre::eyre!(
|
||||
"invalid mount mode '{}'; expected read-only or read-write",
|
||||
other
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
"read-write"
|
||||
};
|
||||
let host_path = resolve_host_path(cwd, host_part);
|
||||
let host_display = relative_to_home(&host_path);
|
||||
let guest_display = if Path::new(guest_part).is_absolute() {
|
||||
guest_part.to_string()
|
||||
} else {
|
||||
format!("/root/{guest_part}")
|
||||
};
|
||||
Ok(tui::MountListRow {
|
||||
host: host_display,
|
||||
guest: guest_display,
|
||||
mode: mode.to_string(),
|
||||
default_mount: if default_mount { "yes" } else { "no" }.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_host_path(cwd: &Path, host: &str) -> PathBuf {
|
||||
if let Some(stripped) = host.strip_prefix("~/") {
|
||||
if let Ok(home) = env::var("HOME") {
|
||||
return PathBuf::from(home).join(stripped);
|
||||
}
|
||||
} else if host == "~" {
|
||||
if let Ok(home) = env::var("HOME") {
|
||||
return PathBuf::from(home);
|
||||
}
|
||||
}
|
||||
let host_path = PathBuf::from(host);
|
||||
if host_path.is_absolute() {
|
||||
host_path
|
||||
} else {
|
||||
cwd.join(host_path)
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -6,9 +6,7 @@ use std::{
|
||||
use color_eyre::Result;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use vibebox::{instance, vm, vm_manager};
|
||||
|
||||
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 3000;
|
||||
use vibebox::{config, instance, vm, vm_manager};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_tracing();
|
||||
@@ -16,14 +14,17 @@ fn main() -> Result<()> {
|
||||
|
||||
tracing::info!("starting vm supervisor");
|
||||
let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
let config = config::load_config(&cwd);
|
||||
let instance_dir = instance::ensure_instance_dir(&cwd)
|
||||
.map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
let _ = instance::touch_last_active(&instance_dir);
|
||||
let args = vm::parse_cli().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
|
||||
let args = vm::VmArg {
|
||||
cpu_count: config.box_cfg.cpu_count,
|
||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||
no_default_mounts: false,
|
||||
mounts: config.box_cfg.mounts.clone(),
|
||||
};
|
||||
let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
|
||||
tracing::info!(auto_shutdown_ms, "vm supervisor config");
|
||||
|
||||
let result = vm_manager::run_manager(args, auto_shutdown_ms);
|
||||
|
||||
+278
-18
@@ -1,38 +1,298 @@
|
||||
use std::{
|
||||
fs, io,
|
||||
env, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::vm::DirectoryShare;
|
||||
|
||||
pub const CONFIG_FILENAME: &str = "vibebox.toml";
|
||||
pub const CONFIG_PATH_ENV: &str = "VIBEBOX_CONFIG_PATH";
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct ProjectConfig {
|
||||
pub auto_shutdown_ms: Option<u64>,
|
||||
const DEFAULT_CPU_COUNT: usize = 2;
|
||||
const DEFAULT_RAM_MB: u64 = 2048;
|
||||
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(rename = "box")]
|
||||
pub box_cfg: BoxConfig,
|
||||
pub supervisor: SupervisorConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
box_cfg: BoxConfig::default(),
|
||||
supervisor: SupervisorConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BoxConfig {
|
||||
pub cpu_count: usize,
|
||||
pub ram_mb: u64,
|
||||
pub mounts: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for BoxConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cpu_count: default_cpu_count(),
|
||||
ram_mb: default_ram_mb(),
|
||||
mounts: default_mounts(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SupervisorConfig {
|
||||
pub auto_shutdown_ms: u64,
|
||||
}
|
||||
|
||||
impl Default for SupervisorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auto_shutdown_ms: default_auto_shutdown_ms(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_cpu_count() -> usize {
|
||||
DEFAULT_CPU_COUNT
|
||||
}
|
||||
|
||||
fn default_ram_mb() -> u64 {
|
||||
DEFAULT_RAM_MB
|
||||
}
|
||||
|
||||
fn default_auto_shutdown_ms() -> u64 {
|
||||
DEFAULT_AUTO_SHUTDOWN_MS
|
||||
}
|
||||
|
||||
fn default_mounts() -> Vec<String> {
|
||||
let home = match std::env::var("HOME") {
|
||||
Ok(home) => PathBuf::from(home),
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut mounts = Vec::new();
|
||||
let codex_host = home.join(".codex");
|
||||
if codex_host.exists() {
|
||||
mounts.push("~/.codex:/usr/local/codex:read-write".to_string());
|
||||
}
|
||||
let claude_host = home.join(".claude");
|
||||
if claude_host.exists() {
|
||||
mounts.push("~/.claude:/usr/local/claude:read-write".to_string());
|
||||
}
|
||||
mounts
|
||||
}
|
||||
|
||||
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() {
|
||||
fs::write(&path, "")?;
|
||||
let default_config = Config::default();
|
||||
let contents = toml::to_string_pretty(&default_config).unwrap_or_default();
|
||||
fs::write(&path, contents)?;
|
||||
tracing::info!(path = %path.display(), "created vibebox config");
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn load_config(
|
||||
project_root: &Path,
|
||||
) -> Result<ProjectConfig, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let path = ensure_config_file(project_root)?;
|
||||
let raw = fs::read_to_string(&path)?;
|
||||
tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
|
||||
if raw.trim().is_empty() {
|
||||
return Ok(ProjectConfig::default());
|
||||
}
|
||||
Ok(toml::from_str::<ProjectConfig>(&raw)?)
|
||||
pub fn load_config(project_root: &Path) -> Config {
|
||||
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}")),
|
||||
};
|
||||
let raw = match fs::read_to_string(&path) {
|
||||
Ok(raw) => raw,
|
||||
Err(err) => die(&format!("failed to read config: {err}")),
|
||||
};
|
||||
let trimmed = raw.trim();
|
||||
tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
|
||||
if trimmed.is_empty() {
|
||||
die(&format!(
|
||||
"config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
let value: toml::Value = match toml::from_str(trimmed) {
|
||||
Ok(value) => value,
|
||||
Err(err) => die(&format!("invalid config: {err}")),
|
||||
};
|
||||
let schema_errors = validate_schema(&value);
|
||||
if !schema_errors.is_empty() {
|
||||
let message = format!(
|
||||
"config file ({}) is missing or invalid fields:\n- {}",
|
||||
path.display(),
|
||||
schema_errors.join("\n- ")
|
||||
);
|
||||
die(&message);
|
||||
}
|
||||
|
||||
let config: Config = match toml::from_str(trimmed) {
|
||||
Ok(config) => config,
|
||||
Err(err) => die(&format!("invalid config: {err}")),
|
||||
};
|
||||
validate_or_exit(&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() {
|
||||
Some(table) => table,
|
||||
None => {
|
||||
errors.push("config must be a table".to_string());
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
match root.get("box") {
|
||||
None => errors.push("missing [box] table".to_string()),
|
||||
Some(value) => match value.as_table() {
|
||||
Some(table) => {
|
||||
validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors);
|
||||
validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors);
|
||||
validate_string_array(
|
||||
table,
|
||||
"mounts",
|
||||
"[box].mounts (array of strings)",
|
||||
&mut errors,
|
||||
);
|
||||
}
|
||||
None => errors.push("[box] must be a table".to_string()),
|
||||
},
|
||||
}
|
||||
|
||||
match root.get("supervisor") {
|
||||
None => errors.push("missing [supervisor] table".to_string()),
|
||||
Some(value) => match value.as_table() {
|
||||
Some(table) => {
|
||||
validate_int(
|
||||
table,
|
||||
"auto_shutdown_ms",
|
||||
"[supervisor].auto_shutdown_ms (integer)",
|
||||
&mut errors,
|
||||
);
|
||||
}
|
||||
None => errors.push("[supervisor] must be a table".to_string()),
|
||||
},
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
|
||||
fn validate_int(table: &toml::value::Table, key: &str, label: &str, errors: &mut Vec<String>) {
|
||||
match table.get(key) {
|
||||
None => errors.push(format!("missing {label}")),
|
||||
Some(value) => {
|
||||
if value.as_integer().is_none() {
|
||||
errors.push(format!("invalid {label}: expected integer"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_string_array(
|
||||
table: &toml::value::Table,
|
||||
key: &str,
|
||||
label: &str,
|
||||
errors: &mut Vec<String>,
|
||||
) {
|
||||
match table.get(key) {
|
||||
None => errors.push(format!("missing {label}")),
|
||||
Some(value) => match value.as_array() {
|
||||
Some(values) => {
|
||||
if values.iter().any(|value| !value.is_str()) {
|
||||
errors.push(format!("invalid {label}: expected array of strings"));
|
||||
}
|
||||
}
|
||||
None => errors.push(format!("invalid {label}: expected array of strings")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_or_exit(config: &Config) {
|
||||
if config.box_cfg.cpu_count == 0 {
|
||||
die("box.cpu_count must be >= 1");
|
||||
}
|
||||
if config.box_cfg.ram_mb == 0 {
|
||||
die("box.ram_mb must be >= 1");
|
||||
}
|
||||
if config.supervisor.auto_shutdown_ms == 0 {
|
||||
die("supervisor.auto_shutdown_ms must be >= 1");
|
||||
}
|
||||
for spec in &config.box_cfg.mounts {
|
||||
if let Err(err) = DirectoryShare::from_mount_spec(spec) {
|
||||
die(&format!("invalid mount spec '{spec}': {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn die(message: &str) -> ! {
|
||||
eprintln!("[vibebox] {message}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
+24
-7
@@ -20,14 +20,13 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
commands,
|
||||
session_manager::{INSTANCE_DIR_NAME, INSTANCE_TOML_FILENAME},
|
||||
session_manager::{INSTANCE_DIR_NAME, INSTANCE_FILENAME},
|
||||
tui::AppState,
|
||||
vm::{self, LoginAction, VmInput},
|
||||
};
|
||||
|
||||
const SSH_KEY_NAME: &str = "ssh_key";
|
||||
#[allow(dead_code)]
|
||||
const SERIAL_LOG_NAME: &str = "serial.log";
|
||||
pub(crate) const SERIAL_LOG_NAME: &str = "serial.log";
|
||||
const DEFAULT_SSH_USER: &str = "vibecoder";
|
||||
const SSH_CONNECT_RETRIES: usize = 30;
|
||||
const SSH_CONNECT_DELAY_MS: u64 = 500;
|
||||
@@ -126,7 +125,7 @@ pub(crate) fn ensure_ssh_keypair(
|
||||
pub(crate) fn load_or_create_instance_config(
|
||||
instance_dir: &Path,
|
||||
) -> Result<InstanceConfig, Box<dyn std::error::Error>> {
|
||||
let config_path = instance_dir.join(INSTANCE_TOML_FILENAME);
|
||||
let config_path = instance_dir.join(INSTANCE_FILENAME);
|
||||
let mut config = if config_path.exists() {
|
||||
let raw = fs::read_to_string(&config_path)?;
|
||||
toml::from_str::<InstanceConfig>(&raw)?
|
||||
@@ -163,11 +162,30 @@ pub(crate) fn load_or_create_instance_config(
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn read_instance_config(
|
||||
instance_dir: &Path,
|
||||
) -> Result<Option<InstanceConfig>, Box<dyn std::error::Error>> {
|
||||
let config_path = instance_dir.join(INSTANCE_FILENAME);
|
||||
if !config_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let raw = fs::read_to_string(&config_path)?;
|
||||
let config = toml::from_str::<InstanceConfig>(&raw)?;
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
pub fn read_instance_vm_ip(
|
||||
instance_dir: &Path,
|
||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
let config = read_instance_config(instance_dir)?;
|
||||
Ok(config.and_then(|cfg| cfg.vm_ipv4))
|
||||
}
|
||||
|
||||
pub fn touch_last_active(instance_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut config = load_or_create_instance_config(instance_dir)?;
|
||||
let now = OffsetDateTime::now_utc().format(&Rfc3339)?;
|
||||
config.last_active = Some(now);
|
||||
write_instance_config(&instance_dir.join(INSTANCE_TOML_FILENAME), &config)?;
|
||||
write_instance_config(&instance_dir.join(INSTANCE_FILENAME), &config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -353,7 +371,6 @@ pub(crate) fn build_ssh_login_actions(
|
||||
vec![LoginAction::Send(setup)]
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn spawn_ssh_io(
|
||||
app: Arc<Mutex<AppState>>,
|
||||
config: Arc<Mutex<InstanceConfig>>,
|
||||
@@ -379,7 +396,7 @@ fn spawn_ssh_io(
|
||||
let ssh_ready = Arc::new(AtomicBool::new(false));
|
||||
let input_tx_holder: Arc<Mutex<Option<Sender<VmInput>>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
let instance_path = instance_dir.join(INSTANCE_TOML_FILENAME);
|
||||
let instance_path = instance_dir.join(INSTANCE_FILENAME);
|
||||
let config_for_output = config.clone();
|
||||
let log_for_output = log_file.clone();
|
||||
let ssh_connected_for_output = ssh_connected.clone();
|
||||
|
||||
+116
-16
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
env, fs,
|
||||
io::{self, Write},
|
||||
os::unix::fs::FileTypeExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@@ -11,9 +12,10 @@ use crate::config::CONFIG_FILENAME;
|
||||
pub const INSTANCE_DIR_NAME: &str = ".vibebox";
|
||||
pub const GLOBAL_CACHE_DIR_NAME: &str = "vibebox";
|
||||
pub const GLOBAL_DIR_NAME: &str = ".vibebox";
|
||||
pub const INSTANCE_TOML_FILENAME: &str = "instance.toml";
|
||||
pub const SESSION_TEMP_PREFIX: &str = "sessions";
|
||||
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)]
|
||||
@@ -21,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)]
|
||||
@@ -42,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")]
|
||||
@@ -100,7 +110,8 @@ impl SessionManager {
|
||||
} else {
|
||||
tracing::warn!(
|
||||
directory = %directory.display(),
|
||||
"missing session id in instance.toml"
|
||||
file = INSTANCE_FILENAME,
|
||||
"missing session id in instance file"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
@@ -158,7 +187,7 @@ impl SessionManager {
|
||||
}
|
||||
|
||||
fn session_path_for(&self, id: &str) -> PathBuf {
|
||||
let filename = format!("{id}.toml");
|
||||
let filename = format!("{id}{SESSION_TOML_SUFFIX}");
|
||||
self.sessions_dir.join(filename)
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
@@ -225,9 +321,7 @@ fn read_session_file(path: &Path) -> Result<SessionEntry, SessionError> {
|
||||
}
|
||||
|
||||
fn read_instance_metadata(directory: &Path) -> Result<InstanceMetadata, SessionError> {
|
||||
let instance_path = directory
|
||||
.join(INSTANCE_DIR_NAME)
|
||||
.join(INSTANCE_TOML_FILENAME);
|
||||
let instance_path = directory.join(INSTANCE_DIR_NAME).join(INSTANCE_FILENAME);
|
||||
if !instance_path.exists() {
|
||||
return Ok(InstanceMetadata::default());
|
||||
}
|
||||
@@ -256,7 +350,7 @@ fn atomic_write(path: &Path, content: &[u8]) -> io::Result<()> {
|
||||
|
||||
fs::create_dir_all(parent)?;
|
||||
let mut temp = tempfile::Builder::new()
|
||||
.prefix(SESSION_TEMP_PREFIX)
|
||||
.prefix(SESSIONS_DIR_NAME)
|
||||
.suffix(SESSION_TOML_SUFFIX)
|
||||
.tempfile_in(parent)?;
|
||||
temp.write_all(content)?;
|
||||
@@ -285,7 +379,7 @@ mod tests {
|
||||
let instance_dir = project_dir.join(INSTANCE_DIR_NAME);
|
||||
fs::create_dir_all(&instance_dir).unwrap();
|
||||
let content = format!("id = \"{id}\"\nlast_active = \"{last_active}\"\n");
|
||||
fs::write(instance_dir.join(INSTANCE_TOML_FILENAME), content).unwrap();
|
||||
fs::write(instance_dir.join(INSTANCE_FILENAME), content).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -306,9 +400,10 @@ mod tests {
|
||||
assert_eq!(dirs[0], project_dir.canonicalize().unwrap());
|
||||
assert!(mgr.index_path().exists());
|
||||
|
||||
let session_path = mgr
|
||||
.index_path()
|
||||
.join("019bf290-cccc-7c23-ba1d-dce7e6d40693.toml");
|
||||
let session_path = mgr.index_path().join(format!(
|
||||
"019bf290-cccc-7c23-ba1d-dce7e6d40693{}",
|
||||
SESSION_TOML_SUFFIX
|
||||
));
|
||||
assert!(session_path.exists());
|
||||
}
|
||||
|
||||
@@ -329,9 +424,10 @@ mod tests {
|
||||
let sessions = mgr.list_sessions().unwrap();
|
||||
assert!(sessions.is_empty());
|
||||
|
||||
let session_path = mgr
|
||||
.index_path()
|
||||
.join("019bf290-cccc-7c23-ba1d-dce7e6d40693.toml");
|
||||
let session_path = mgr.index_path().join(format!(
|
||||
"019bf290-cccc-7c23-ba1d-dce7e6d40693{}",
|
||||
SESSION_TOML_SUFFIX
|
||||
));
|
||||
assert!(!session_path.exists());
|
||||
}
|
||||
|
||||
@@ -340,7 +436,11 @@ mod tests {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let mgr = manager(&temp);
|
||||
fs::create_dir_all(mgr.index_path()).unwrap();
|
||||
fs::write(mgr.index_path().join("bad.toml"), "not toml").unwrap();
|
||||
fs::write(
|
||||
mgr.index_path().join(format!("bad{SESSION_TOML_SUFFIX}")),
|
||||
"not toml",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let err = mgr.list_sessions().unwrap_err();
|
||||
assert!(matches!(err, SessionError::TomlDe(_)));
|
||||
|
||||
+217
-1
@@ -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,31 @@ 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)]
|
||||
pub struct MountListRow {
|
||||
pub host: String,
|
||||
pub guest: String,
|
||||
pub mode: String,
|
||||
pub default_mount: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkListRow {
|
||||
pub network_type: String,
|
||||
pub vm_ip: String,
|
||||
pub host_to_vm: String,
|
||||
pub vm_to_host: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
struct PageLayout {
|
||||
header: Rect,
|
||||
@@ -174,6 +199,197 @@ 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 render_mounts_table(rows: &[MountListRow]) -> 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);
|
||||
|
||||
render_mounts_table_into(rows, 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 render_explain_tables(mounts: &[MountListRow], networks: &[NetworkListRow]) -> Result<()> {
|
||||
let (width, _) = crossterm::terminal::size()?;
|
||||
if width == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mounts_height = if mounts.is_empty() {
|
||||
0
|
||||
} else {
|
||||
(mounts.len() as u16).saturating_add(3)
|
||||
};
|
||||
let networks_height = if networks.is_empty() {
|
||||
0
|
||||
} else {
|
||||
(networks.len() as u16).saturating_add(3)
|
||||
};
|
||||
let gap = if mounts_height > 0 && networks_height > 0 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let total_height = mounts_height
|
||||
.saturating_add(gap)
|
||||
.saturating_add(networks_height);
|
||||
if total_height == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, total_height));
|
||||
let mut y = 0u16;
|
||||
|
||||
if mounts_height > 0 {
|
||||
let area = Rect::new(0, y, width, mounts_height);
|
||||
render_mounts_table_into(mounts, area, &mut buffer);
|
||||
y = y.saturating_add(mounts_height).saturating_add(gap);
|
||||
}
|
||||
|
||||
if networks_height > 0 {
|
||||
let area = Rect::new(0, y, width, networks_height);
|
||||
render_networks_table_into(networks, 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(())
|
||||
}
|
||||
|
||||
fn render_mounts_table_into(rows: &[MountListRow], area: Rect, buffer: &mut Buffer) {
|
||||
let header = Row::new(vec![
|
||||
Cell::from("Host"),
|
||||
Cell::from("Guest"),
|
||||
Cell::from("Mode"),
|
||||
Cell::from(""),
|
||||
Cell::from("Default"),
|
||||
])
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
|
||||
let table_rows = rows.iter().map(|row| {
|
||||
Row::new(vec![
|
||||
Cell::from(row.host.clone()),
|
||||
Cell::from(row.guest.clone()),
|
||||
Cell::from(row.mode.clone()),
|
||||
Cell::from(""),
|
||||
Cell::from(row.default_mount.clone()),
|
||||
])
|
||||
});
|
||||
|
||||
let table = Table::new(
|
||||
table_rows,
|
||||
[
|
||||
Constraint::Min(24),
|
||||
Constraint::Min(24),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(8),
|
||||
],
|
||||
)
|
||||
.header(header)
|
||||
.block(Block::default().title("Mounts").borders(Borders::ALL))
|
||||
.column_spacing(1);
|
||||
|
||||
table.render(area, buffer);
|
||||
}
|
||||
|
||||
fn render_networks_table_into(rows: &[NetworkListRow], area: Rect, buffer: &mut Buffer) {
|
||||
let header = Row::new(vec![
|
||||
Cell::from("Type"),
|
||||
Cell::from("VM IP"),
|
||||
Cell::from("Host \u{2192} VM"),
|
||||
Cell::from("VM \u{2192} Host"),
|
||||
])
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
|
||||
let table_rows = rows.iter().map(|row| {
|
||||
Row::new(vec![
|
||||
Cell::from(row.network_type.clone()),
|
||||
Cell::from(row.vm_ip.clone()),
|
||||
Cell::from(row.host_to_vm.clone()),
|
||||
Cell::from(row.vm_to_host.clone()),
|
||||
])
|
||||
});
|
||||
|
||||
let table = Table::new(
|
||||
table_rows,
|
||||
[
|
||||
Constraint::Length(8),
|
||||
Constraint::Length(16),
|
||||
Constraint::Min(24),
|
||||
Constraint::Min(20),
|
||||
],
|
||||
)
|
||||
.header(header)
|
||||
.block(Block::default().title("Network").borders(Borders::ALL))
|
||||
.column_spacing(1);
|
||||
|
||||
table.render(area, buffer);
|
||||
}
|
||||
|
||||
pub fn passthrough_vm_io(
|
||||
app: Arc<Mutex<AppState>>,
|
||||
output_monitor: Arc<vm::OutputMonitor>,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsString,
|
||||
fs,
|
||||
env, fs,
|
||||
io::{self, Write},
|
||||
os::{
|
||||
fd::RawFd,
|
||||
@@ -25,7 +23,6 @@ use std::{
|
||||
|
||||
use block2::RcBlock;
|
||||
use dispatch2::DispatchQueue;
|
||||
use lexopt::prelude::*;
|
||||
use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_foundation::*;
|
||||
use objc2_virtualization::*;
|
||||
@@ -40,9 +37,12 @@ const DEFAULT_CPU_COUNT: usize = 2;
|
||||
const DEFAULT_RAM_MB: u64 = 2048;
|
||||
const DEFAULT_RAM_BYTES: u64 = DEFAULT_RAM_MB * BYTES_PER_MB;
|
||||
const START_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const DEFAULT_EXPECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
|
||||
const PROVISION_SCRIPT: &str = include_str!("provision.sh");
|
||||
const PROVISION_SCRIPT_NAME: &str = "provision.sh";
|
||||
const DEFAULT_RAW_NAME: &str = "default.raw";
|
||||
const INSTANCE_RAW_NAME: &str = "instance.raw";
|
||||
const BASE_DISK_RAW_NAME: &str = "disk.raw";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum LoginAction {
|
||||
@@ -83,7 +83,7 @@ impl DirectoryShare {
|
||||
if parts.len() < 2 || parts.len() > 3 {
|
||||
return Err(format!("Invalid mount spec: {spec}").into());
|
||||
}
|
||||
let host = PathBuf::from(parts[0]);
|
||||
let host = expand_tilde_path(parts[0]);
|
||||
let guest = PathBuf::from(parts[1]);
|
||||
let read_only = if parts.len() == 3 {
|
||||
match parts[2] {
|
||||
@@ -117,7 +117,27 @@ impl DirectoryShare {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_with_args<F>(args: CliArgs, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
|
||||
fn expand_tilde_path(value: &str) -> PathBuf {
|
||||
if let Some(stripped) = value.strip_prefix("~/") {
|
||||
if let Ok(home) = env::var("HOME") {
|
||||
return PathBuf::from(home).join(stripped);
|
||||
}
|
||||
} else if value == "~" {
|
||||
if let Ok(home) = env::var("HOME") {
|
||||
return PathBuf::from(home);
|
||||
}
|
||||
}
|
||||
PathBuf::from(value)
|
||||
}
|
||||
|
||||
pub struct VmArg {
|
||||
pub cpu_count: usize,
|
||||
pub ram_bytes: u64,
|
||||
pub no_default_mounts: bool,
|
||||
pub mounts: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn run_with_args<F>(args: VmArg, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
|
||||
where
|
||||
F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
|
||||
{
|
||||
@@ -125,7 +145,7 @@ where
|
||||
}
|
||||
|
||||
pub(crate) fn run_with_args_and_extras<F>(
|
||||
args: CliArgs,
|
||||
args: VmArg,
|
||||
io_handler: F,
|
||||
extra_login_actions: Vec<LoginAction>,
|
||||
extra_directory_shares: Vec<DirectoryShare>,
|
||||
@@ -158,8 +178,8 @@ where
|
||||
basename_compressed.trim_end_matches(".tar.xz")
|
||||
));
|
||||
|
||||
let default_raw = cache_dir.join("default.raw");
|
||||
let instance_raw = instance_dir.join("instance.raw");
|
||||
let default_raw = cache_dir.join(DEFAULT_RAW_NAME);
|
||||
let instance_raw = instance_dir.join(INSTANCE_RAW_NAME);
|
||||
|
||||
// Prepare system-wide directories
|
||||
fs::create_dir_all(&cache_dir)?;
|
||||
@@ -168,22 +188,14 @@ where
|
||||
let mise_directory_share =
|
||||
DirectoryShare::new(guest_mise_cache, "/root/.local/share/mise".into(), false)?;
|
||||
|
||||
let disk_path = if let Some(path) = args.disk {
|
||||
if !path.exists() {
|
||||
return Err(format!("Disk image does not exist: {}", path.display()).into());
|
||||
}
|
||||
path
|
||||
} else {
|
||||
ensure_default_image(
|
||||
&base_raw,
|
||||
&base_compressed,
|
||||
&default_raw,
|
||||
std::slice::from_ref(&mise_directory_share),
|
||||
)?;
|
||||
ensure_instance_disk(&instance_raw, &default_raw)?;
|
||||
|
||||
instance_raw
|
||||
};
|
||||
ensure_default_image(
|
||||
&base_raw,
|
||||
&base_compressed,
|
||||
&default_raw,
|
||||
std::slice::from_ref(&mise_directory_share),
|
||||
)?;
|
||||
ensure_instance_disk(&instance_raw, &default_raw)?;
|
||||
let disk_path = instance_raw;
|
||||
|
||||
let mut login_actions = Vec::new();
|
||||
let mut directory_shares = Vec::new();
|
||||
@@ -207,22 +219,6 @@ where
|
||||
);
|
||||
|
||||
directory_shares.push(mise_directory_share);
|
||||
|
||||
// Add default shares, if they exist
|
||||
for share in [
|
||||
DirectoryShare::new(home.join(".codex"), "/usr/local/codex".into(), false),
|
||||
DirectoryShare::new(home.join(".claude"), "/usr/local/claude".into(), false),
|
||||
DirectoryShare::new(
|
||||
"/Users/zhangjie/Documents/Code/CompletePrograms/vibebox/.ssh".into(),
|
||||
"/usr/local/ssh".into(),
|
||||
true,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
directory_shares.push(share)
|
||||
}
|
||||
}
|
||||
|
||||
directory_shares.extend(extra_directory_shares);
|
||||
@@ -237,9 +233,6 @@ where
|
||||
|
||||
login_actions.extend(extra_login_actions);
|
||||
|
||||
// Any user-provided login actions must come after our system ones
|
||||
login_actions.extend(args.login_actions);
|
||||
|
||||
run_vm_with_io(
|
||||
&disk_path,
|
||||
&login_actions,
|
||||
@@ -250,156 +243,6 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
pub fn print_help() {
|
||||
println!(
|
||||
"Vibe is a quick way to spin up a Linux virtual machine on Mac to sandbox LLM agents.
|
||||
|
||||
vibe [OPTIONS] [disk-image.raw]
|
||||
|
||||
Options
|
||||
|
||||
--help Print this help message.
|
||||
--version Print the version (commit SHA).
|
||||
--no-default-mounts Disable all default mounts.
|
||||
--mount host-path:guest-path[:read-only | :read-write] Mount `host-path` inside VM at `guest-path`.
|
||||
Defaults to read-write.
|
||||
Errors if host-path does not exist.
|
||||
--cpus <count> Number of virtual CPUs (default {DEFAULT_CPU_COUNT}).
|
||||
--ram <megabytes> RAM size in megabytes (default {DEFAULT_RAM_MB}).
|
||||
--script <path/to/script.sh> Run script in VM.
|
||||
--send <some-command> Type `some-command` followed by newline into the VM.
|
||||
--expect <string> [timeout-seconds] Wait for `string` to appear in console output before executing next `--script` or `--send`.
|
||||
If `string` does not appear within timeout (default 30 seconds), shutdown VM with error.
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn print_version() {
|
||||
println!("Vibe");
|
||||
println!("https://github.com/lynaghk/vibe/");
|
||||
println!("Git SHA: {}", env!("GIT_SHA"));
|
||||
}
|
||||
|
||||
pub struct CliArgs {
|
||||
disk: Option<PathBuf>,
|
||||
version: bool,
|
||||
help: bool,
|
||||
no_default_mounts: bool,
|
||||
mounts: Vec<String>,
|
||||
login_actions: Vec<LoginAction>,
|
||||
cpu_count: usize,
|
||||
ram_bytes: u64,
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
pub fn version(&self) -> bool {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn help(&self) -> bool {
|
||||
self.help
|
||||
}
|
||||
|
||||
pub fn cpu_count(&self) -> usize {
|
||||
self.cpu_count
|
||||
}
|
||||
|
||||
pub fn ram_bytes(&self) -> u64 {
|
||||
self.ram_bytes
|
||||
}
|
||||
|
||||
pub fn ram_mb(&self) -> u64 {
|
||||
self.ram_bytes / BYTES_PER_MB
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_cli() -> Result<CliArgs, Box<dyn std::error::Error>> {
|
||||
parse_cli_from(env::args_os())
|
||||
}
|
||||
|
||||
pub fn parse_cli_from<I>(args: I) -> Result<CliArgs, Box<dyn std::error::Error>>
|
||||
where
|
||||
I: IntoIterator<Item = OsString>,
|
||||
{
|
||||
fn os_to_string(value: OsString, flag: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
value
|
||||
.into_string()
|
||||
.map_err(|_| format!("{flag} expects valid UTF-8").into())
|
||||
}
|
||||
|
||||
let mut parser = lexopt::Parser::from_iter(args);
|
||||
let mut disk = None;
|
||||
let mut version = false;
|
||||
let mut help = false;
|
||||
let mut no_default_mounts = false;
|
||||
let mut mounts = Vec::new();
|
||||
let mut login_actions = Vec::new();
|
||||
let mut script_index = 0;
|
||||
let mut cpu_count = DEFAULT_CPU_COUNT;
|
||||
let mut ram_bytes = DEFAULT_RAM_BYTES;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("version") => version = true,
|
||||
Long("help") | Short('h') => help = true,
|
||||
Long("no-default-mounts") => no_default_mounts = true,
|
||||
Long("cpus") => {
|
||||
let value = os_to_string(parser.value()?, "--cpus")?.parse()?;
|
||||
if value == 0 {
|
||||
return Err("--cpus must be >= 1".into());
|
||||
}
|
||||
cpu_count = value;
|
||||
}
|
||||
Long("ram") => {
|
||||
let value: u64 = os_to_string(parser.value()?, "--ram")?.parse()?;
|
||||
if value == 0 {
|
||||
return Err("--ram must be >= 1".into());
|
||||
}
|
||||
ram_bytes = value * BYTES_PER_MB;
|
||||
}
|
||||
Long("mount") => {
|
||||
mounts.push(os_to_string(parser.value()?, "--mount")?);
|
||||
}
|
||||
Long("script") => {
|
||||
login_actions.push(Script {
|
||||
path: os_to_string(parser.value()?, "--script")?.into(),
|
||||
index: script_index,
|
||||
});
|
||||
script_index += 1;
|
||||
}
|
||||
Long("send") => {
|
||||
login_actions.push(Send(os_to_string(parser.value()?, "--send")?));
|
||||
}
|
||||
Long("expect") => {
|
||||
let text = os_to_string(parser.value()?, "--expect")?;
|
||||
let timeout = match parser.optional_value() {
|
||||
Some(value) => Duration::from_secs(os_to_string(value, "--expect")?.parse()?),
|
||||
None => DEFAULT_EXPECT_TIMEOUT,
|
||||
};
|
||||
login_actions.push(Expect { text, timeout });
|
||||
}
|
||||
Value(value) => {
|
||||
if disk.is_some() {
|
||||
return Err("Only one disk path may be provided".into());
|
||||
}
|
||||
disk = Some(PathBuf::from(value));
|
||||
}
|
||||
_ => return Err(arg.unexpected().into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CliArgs {
|
||||
disk,
|
||||
version,
|
||||
help,
|
||||
no_default_mounts,
|
||||
mounts,
|
||||
login_actions,
|
||||
cpu_count,
|
||||
ram_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn script_command_from_path(
|
||||
path: &Path,
|
||||
index: usize,
|
||||
@@ -629,7 +472,11 @@ fn ensure_base_image(
|
||||
|
||||
println!("Decompressing base image...");
|
||||
let status = Command::new("tar")
|
||||
.args(["-xOf", &base_compressed.to_string_lossy(), "disk.raw"])
|
||||
.args([
|
||||
"-xOf",
|
||||
&base_compressed.to_string_lossy(),
|
||||
BASE_DISK_RAW_NAME,
|
||||
])
|
||||
.stdout(std::fs::File::create(base_raw)?)
|
||||
.status()?;
|
||||
|
||||
@@ -655,7 +502,7 @@ fn ensure_default_image(
|
||||
println!("Configuring base image...");
|
||||
fs::copy(base_raw, default_raw)?;
|
||||
|
||||
let provision_command = script_command_from_content("provision.sh", PROVISION_SCRIPT)?;
|
||||
let provision_command = script_command_from_content(PROVISION_SCRIPT_NAME, PROVISION_SCRIPT)?;
|
||||
run_vm(
|
||||
default_raw,
|
||||
&[Send(provision_command)],
|
||||
|
||||
+60
-11
@@ -15,24 +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,
|
||||
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) {
|
||||
@@ -45,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(),
|
||||
@@ -89,11 +95,12 @@ pub fn ensure_manager(
|
||||
}
|
||||
|
||||
pub fn run_manager(
|
||||
args: vm::CliArgs,
|
||||
args: vm::VmArg,
|
||||
auto_shutdown_ms: u64,
|
||||
) -> 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,
|
||||
@@ -111,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();
|
||||
@@ -131,8 +139,11 @@ 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");
|
||||
let log_path = instance_dir.join(VM_MANAGER_LOG_NAME);
|
||||
let log_file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
@@ -153,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();
|
||||
@@ -280,7 +329,7 @@ fn spawn_manager_io(
|
||||
vm_output_fd: std::os::unix::io::OwnedFd,
|
||||
vm_input_fd: std::os::unix::io::OwnedFd,
|
||||
) -> vm::IoContext {
|
||||
let log_path = instance_dir.join("serial.log");
|
||||
let log_path = instance_dir.join(SERIAL_LOG_NAME);
|
||||
let log_file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
@@ -289,7 +338,7 @@ fn spawn_manager_io(
|
||||
.ok()
|
||||
.map(|file| Arc::new(Mutex::new(file)));
|
||||
|
||||
let instance_path = instance_dir.join("instance.toml");
|
||||
let instance_path = instance_dir.join(INSTANCE_FILENAME);
|
||||
let config_for_output = config.clone();
|
||||
let log_for_output = log_file.clone();
|
||||
let mut line_buf = String::new();
|
||||
@@ -390,7 +439,7 @@ struct ManagerOptions {
|
||||
trait VmExecutor {
|
||||
fn run_vm(
|
||||
&self,
|
||||
args: vm::CliArgs,
|
||||
args: vm::VmArg,
|
||||
extra_login_actions: Vec<LoginAction>,
|
||||
extra_shares: Vec<DirectoryShare>,
|
||||
config: Arc<Mutex<InstanceConfig>>,
|
||||
@@ -404,7 +453,7 @@ struct RealVmExecutor;
|
||||
impl VmExecutor for RealVmExecutor {
|
||||
fn run_vm(
|
||||
&self,
|
||||
args: vm::CliArgs,
|
||||
args: vm::VmArg,
|
||||
extra_login_actions: Vec<LoginAction>,
|
||||
extra_shares: Vec<DirectoryShare>,
|
||||
config: Arc<Mutex<InstanceConfig>>,
|
||||
@@ -433,7 +482,7 @@ impl VmExecutor for RealVmExecutor {
|
||||
|
||||
fn run_manager_with(
|
||||
project_root: &Path,
|
||||
args: vm::CliArgs,
|
||||
args: vm::VmArg,
|
||||
auto_shutdown_ms: u64,
|
||||
executor: &dyn VmExecutor,
|
||||
options: ManagerOptions,
|
||||
@@ -465,7 +514,7 @@ fn run_manager_with(
|
||||
let mut config = load_or_create_instance_config(&instance_dir)?;
|
||||
if config.vm_ipv4.is_some() {
|
||||
config.vm_ipv4 = None;
|
||||
write_instance_config(&instance_dir.join("instance.toml"), &config)?;
|
||||
write_instance_config(&instance_dir.join(INSTANCE_FILENAME), &config)?;
|
||||
}
|
||||
let config = Arc::new(Mutex::new(config));
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[box]
|
||||
cpu_count = 2
|
||||
ram_mb = 2048
|
||||
mounts = [
|
||||
"~/.codex:/usr/local/codex:read-write",
|
||||
"~/.claude:/usr/local/claude:read-write",
|
||||
]
|
||||
|
||||
[supervisor]
|
||||
auto_shutdown_ms = 20000
|
||||
|
||||
Reference in New Issue
Block a user