argument parsing, skeleton structure, basic project
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
474
Cargo.lock
generated
Normal file
474
Cargo.lock
generated
Normal file
@@ -0,0 +1,474 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
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 = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "p"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"dirs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p-agent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
3
Cargo.toml
Normal file
3
Cargo.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
members = ["p", "agent"]
|
||||
resolver = "2"
|
||||
11
agent/Cargo.toml
Normal file
11
agent/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "p-agent"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "p-agent"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
4
agent/src/main.rs
Normal file
4
agent/src/main.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
println!("p-agent — worker-side agent for p");
|
||||
println!("(not yet implemented)");
|
||||
}
|
||||
20
p/Cargo.toml
Normal file
20
p/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "p"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "p"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# CLI
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
# Error handling
|
||||
anyhow = "1"
|
||||
# Config file (YAML) + job records (JSON)
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
serde_json = "1"
|
||||
# Platform directories (~/.config, ~/.local/share)
|
||||
dirs = "5"
|
||||
86
p/src/cli.rs
Normal file
86
p/src/cli.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "p",
|
||||
about = "Push jobs to remote worker machines",
|
||||
long_about = "\
|
||||
Push jobs to remote worker machines with directory sync and attach/detach support.
|
||||
|
||||
Run a command on the default worker:
|
||||
p -- <command>
|
||||
|
||||
Run on a specific worker:
|
||||
p <worker> -- <command>
|
||||
|
||||
Skip directory sync:
|
||||
p -n -- <command>"
|
||||
)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// List all jobs across all workers
|
||||
Ls,
|
||||
|
||||
/// Re-attach to the console of a running job
|
||||
Attach {
|
||||
/// Job ID or unambiguous prefix
|
||||
job_id: String,
|
||||
},
|
||||
|
||||
/// Print captured output of a job (running or finished)
|
||||
Logs {
|
||||
/// Job ID or unambiguous prefix
|
||||
job_id: String,
|
||||
/// Follow output in real time (running jobs only)
|
||||
#[arg(short, long)]
|
||||
follow: bool,
|
||||
},
|
||||
|
||||
/// Kill a running job
|
||||
Stop {
|
||||
/// Job ID or unambiguous prefix
|
||||
job_id: String,
|
||||
},
|
||||
|
||||
/// Copy a file or directory from a job's work directory back to the client
|
||||
Pull {
|
||||
/// Job ID or unambiguous prefix
|
||||
job_id: String,
|
||||
/// Path relative to the job's work directory on the worker
|
||||
remote_path: String,
|
||||
/// Local destination (defaults to current directory)
|
||||
local_dest: Option<String>,
|
||||
},
|
||||
|
||||
/// Remove a job record and its remote work directory
|
||||
Rm {
|
||||
/// Job ID or unambiguous prefix
|
||||
job_id: String,
|
||||
/// Force removal even if the job is still running
|
||||
#[arg(short, long)]
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// Register a remote worker
|
||||
Register {
|
||||
/// SSH connection string: user@host, user@host:port, or an SSH config alias
|
||||
connection: String,
|
||||
/// Worker name (defaults to the hostname part of the connection string)
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
},
|
||||
|
||||
/// List registered workers and their reachability status
|
||||
Workers,
|
||||
|
||||
/// Set the default worker
|
||||
Default {
|
||||
/// Worker name
|
||||
worker: String,
|
||||
},
|
||||
}
|
||||
6
p/src/commands/attach.rs
Normal file
6
p/src/commands/attach.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute(job_id: &str) -> Result<()> {
|
||||
let _ = job_id;
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
6
p/src/commands/logs.rs
Normal file
6
p/src/commands/logs.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute(job_id: &str, follow: bool) -> Result<()> {
|
||||
let _ = (job_id, follow);
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
5
p/src/commands/ls.rs
Normal file
5
p/src/commands/ls.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute() -> Result<()> {
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
10
p/src/commands/mod.rs
Normal file
10
p/src/commands/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
pub mod attach;
|
||||
pub mod logs;
|
||||
pub mod ls;
|
||||
pub mod pull;
|
||||
pub mod register;
|
||||
pub mod rm;
|
||||
pub mod run;
|
||||
pub mod set_default;
|
||||
pub mod stop;
|
||||
pub mod workers;
|
||||
6
p/src/commands/pull.rs
Normal file
6
p/src/commands/pull.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute(job_id: &str, remote_path: &str, local_dest: Option<&str>) -> Result<()> {
|
||||
let _ = (job_id, remote_path, local_dest);
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
6
p/src/commands/register.rs
Normal file
6
p/src/commands/register.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute(connection: &str, name: Option<&str>) -> Result<()> {
|
||||
let _ = (connection, name);
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
6
p/src/commands/rm.rs
Normal file
6
p/src/commands/rm.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute(job_id: &str, force: bool) -> Result<()> {
|
||||
let _ = (job_id, force);
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
6
p/src/commands/run.rs
Normal file
6
p/src/commands/run.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute(worker: Option<&str>, cmd: Vec<String>, no_sync: bool) -> Result<()> {
|
||||
let _ = (worker, cmd, no_sync);
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
6
p/src/commands/set_default.rs
Normal file
6
p/src/commands/set_default.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute(worker: &str) -> Result<()> {
|
||||
let _ = worker;
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
6
p/src/commands/stop.rs
Normal file
6
p/src/commands/stop.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute(job_id: &str) -> Result<()> {
|
||||
let _ = job_id;
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
5
p/src/commands/workers.rs
Normal file
5
p/src/commands/workers.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn execute() -> Result<()> {
|
||||
anyhow::bail!("not yet implemented")
|
||||
}
|
||||
75
p/src/config.rs
Normal file
75
p/src/config.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// ── Config types ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||
pub struct Config {
|
||||
pub default_worker: Option<String>,
|
||||
#[serde(default)]
|
||||
pub workers: Vec<WorkerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct WorkerConfig {
|
||||
pub name: String,
|
||||
pub connection: String,
|
||||
}
|
||||
|
||||
// ── Path helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn config_path() -> Result<PathBuf> {
|
||||
Ok(dirs::config_dir()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not determine config directory"))?
|
||||
.join("p")
|
||||
.join("config.yaml"))
|
||||
}
|
||||
|
||||
// ── Load / save ───────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn load() -> Result<Config> {
|
||||
let path = config_path()?;
|
||||
if !path.exists() {
|
||||
return Ok(Config::default());
|
||||
}
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.map_err(|e| anyhow::anyhow!("failed to read {}: {}", path.display(), e))?;
|
||||
serde_yaml::from_str(&content)
|
||||
.map_err(|e| anyhow::anyhow!("failed to parse config: {}", e))
|
||||
}
|
||||
|
||||
pub fn save(config: &Config) -> Result<()> {
|
||||
let path = config_path()?;
|
||||
std::fs::create_dir_all(path.parent().unwrap_or(Path::new(".")))?;
|
||||
let content = serde_yaml::to_string(config)
|
||||
.map_err(|e| anyhow::anyhow!("failed to serialise config: {}", e))?;
|
||||
std::fs::write(&path, content)
|
||||
.map_err(|e| anyhow::anyhow!("failed to write {}: {}", path.display(), e))
|
||||
}
|
||||
|
||||
// ── Lookup helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
impl Config {
|
||||
pub fn get_worker(&self, name: &str) -> Option<&WorkerConfig> {
|
||||
self.workers.iter().find(|w| w.name == name)
|
||||
}
|
||||
|
||||
pub fn default_worker(&self) -> Option<&WorkerConfig> {
|
||||
self.get_worker(self.default_worker.as_deref()?)
|
||||
}
|
||||
|
||||
/// Resolve a worker by optional name, falling back to the default.
|
||||
pub fn resolve_worker(&self, name: Option<&str>) -> Result<&WorkerConfig> {
|
||||
match name {
|
||||
Some(n) => self
|
||||
.get_worker(n)
|
||||
.ok_or_else(|| anyhow::anyhow!("unknown worker '{}'", n)),
|
||||
None => self
|
||||
.default_worker()
|
||||
.ok_or_else(|| anyhow::anyhow!(
|
||||
"no default worker configured — run 'p register <connection>' first"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
113
p/src/db.rs
Normal file
113
p/src/db.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
/// Per-job JSON storage under ~/.local/share/p/jobs/<uuid>.json
|
||||
///
|
||||
/// Each job is a self-contained file. This makes records human-readable,
|
||||
/// trivially debuggable with `cat`, and requires no schema migrations.
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::job::Job;
|
||||
|
||||
// ── Paths ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn jobs_dir() -> Result<PathBuf> {
|
||||
Ok(dirs::data_local_dir()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not determine data directory"))?
|
||||
.join("p")
|
||||
.join("jobs"))
|
||||
}
|
||||
|
||||
fn job_path(id: &str) -> Result<PathBuf> {
|
||||
Ok(jobs_dir()?.join(format!("{}.json", id)))
|
||||
}
|
||||
|
||||
// ── CRUD ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Write (or overwrite) a job record to disk.
|
||||
pub fn save(job: &Job) -> Result<()> {
|
||||
let dir = jobs_dir()?;
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let content = serde_json::to_string_pretty(job).context("failed to serialise job")?;
|
||||
std::fs::write(dir.join(format!("{}.json", job.id)), content)
|
||||
.with_context(|| format!("failed to write job {}", job.id))
|
||||
}
|
||||
|
||||
/// Load a job by exact UUID.
|
||||
pub fn load(id: &str) -> Result<Option<Job>> {
|
||||
let path = job_path(id)?;
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("failed to read job {}", id))?;
|
||||
serde_json::from_str(&content)
|
||||
.with_context(|| format!("failed to parse job {}", id))
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
/// Find a job by UUID prefix. Errors if the prefix is ambiguous.
|
||||
pub fn find(prefix: &str) -> Result<Option<Job>> {
|
||||
let dir = jobs_dir()?;
|
||||
if !dir.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let matches: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.context("failed to read jobs directory")?
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| {
|
||||
p.extension().and_then(|e| e.to_str()) == Some("json")
|
||||
&& p.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|stem| stem.starts_with(prefix))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
match matches.len() {
|
||||
0 => Ok(None),
|
||||
1 => {
|
||||
let content = std::fs::read_to_string(&matches[0])
|
||||
.with_context(|| format!("failed to read {:?}", matches[0]))?;
|
||||
serde_json::from_str(&content)
|
||||
.context("failed to parse job")
|
||||
.map(Some)
|
||||
}
|
||||
n => anyhow::bail!("ambiguous job ID '{}' matches {} jobs", prefix, n),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all jobs, sorted newest-first.
|
||||
pub fn list() -> Result<Vec<Job>> {
|
||||
let dir = jobs_dir()?;
|
||||
if !dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut jobs = Vec::new();
|
||||
for entry in std::fs::read_dir(&dir).context("failed to read jobs directory")? {
|
||||
let path = entry?.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("json") {
|
||||
continue;
|
||||
}
|
||||
match std::fs::read_to_string(&path).and_then(|c| {
|
||||
serde_json::from_str::<Job>(&c).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
}) {
|
||||
Ok(job) => jobs.push(job),
|
||||
Err(e) => eprintln!("warning: skipping {:?}: {}", path, e),
|
||||
}
|
||||
}
|
||||
|
||||
jobs.sort_by(|a, b| b.started_at.cmp(&a.started_at));
|
||||
Ok(jobs)
|
||||
}
|
||||
|
||||
/// Delete a job record from disk.
|
||||
pub fn delete(id: &str) -> Result<()> {
|
||||
let path = job_path(id)?;
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path)
|
||||
.with_context(|| format!("failed to delete job {}", id))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
44
p/src/job.rs
Normal file
44
p/src/job.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A job submitted to a remote worker.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Job {
|
||||
pub id: String,
|
||||
pub worker: String,
|
||||
pub cwd: String,
|
||||
pub command: String,
|
||||
pub started_at: i64,
|
||||
pub finished_at: Option<i64>,
|
||||
pub exit_code: Option<i32>,
|
||||
pub status: JobStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum JobStatus {
|
||||
Running,
|
||||
Done,
|
||||
Failed,
|
||||
/// Status not yet reconciled with the worker.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl JobStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Running => "running",
|
||||
Self::Done => "done",
|
||||
Self::Failed => "failed",
|
||||
Self::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"running" => Self::Running,
|
||||
"done" => Self::Done,
|
||||
"failed" => Self::Failed,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
67
p/src/main.rs
Normal file
67
p/src/main.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
mod cli;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod db;
|
||||
mod job;
|
||||
mod ssh;
|
||||
mod sync;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let raw: Vec<String> = std::env::args().collect();
|
||||
|
||||
// `p [flags] [worker] -- <command...>`
|
||||
// We detect the "--" separator manually so clap never sees it.
|
||||
if let Some(sep) = raw.iter().position(|a| a == "--") {
|
||||
let pre = &raw[1..sep];
|
||||
let cmd: Vec<String> = raw[sep + 1..].to_vec();
|
||||
|
||||
if cmd.is_empty() {
|
||||
eprintln!("error: no command specified after --");
|
||||
eprintln!("usage: p [-n] [<worker>] -- <command>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let mut no_sync = false;
|
||||
let mut worker: Option<String> = None;
|
||||
|
||||
for arg in pre {
|
||||
match arg.as_str() {
|
||||
"-n" | "--no-sync" => no_sync = true,
|
||||
flag if flag.starts_with('-') => {
|
||||
eprintln!("error: unknown flag '{}'\nusage: p [-n] [<worker>] -- <command>", flag);
|
||||
std::process::exit(1);
|
||||
}
|
||||
name => {
|
||||
if worker.is_some() {
|
||||
eprintln!("error: multiple worker names before '--'");
|
||||
std::process::exit(1);
|
||||
}
|
||||
worker = Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commands::run::execute(worker.as_deref(), cmd, no_sync);
|
||||
}
|
||||
|
||||
// All other subcommands go through clap.
|
||||
let cli = cli::Cli::parse();
|
||||
match cli.command {
|
||||
cli::Command::Ls => commands::ls::execute(),
|
||||
cli::Command::Attach { job_id } => commands::attach::execute(&job_id),
|
||||
cli::Command::Logs { job_id, follow } => commands::logs::execute(&job_id, follow),
|
||||
cli::Command::Stop { job_id } => commands::stop::execute(&job_id),
|
||||
cli::Command::Pull { job_id, remote_path, local_dest } => {
|
||||
commands::pull::execute(&job_id, &remote_path, local_dest.as_deref())
|
||||
}
|
||||
cli::Command::Rm { job_id, force } => commands::rm::execute(&job_id, force),
|
||||
cli::Command::Register { connection, name } => {
|
||||
commands::register::execute(&connection, name.as_deref())
|
||||
}
|
||||
cli::Command::Workers => commands::workers::execute(),
|
||||
cli::Command::Default { worker } => commands::set_default::execute(&worker),
|
||||
}
|
||||
}
|
||||
45
p/src/ssh.rs
Normal file
45
p/src/ssh.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
/// SSH helpers: run commands, check reachability, poll job status.
|
||||
/// Full implementation in the run-command commit.
|
||||
use crate::config::WorkerConfig;
|
||||
|
||||
/// Parse a connection string into (user@host, optional port).
|
||||
/// Handles "user@host:port", "user@host", "host:port", "host".
|
||||
pub fn parse_connection(conn: &str) -> (String, Option<u16>) {
|
||||
// Only treat the last ':something' as a port if 'something' is a number.
|
||||
// This avoids misinterpreting plain SSH config aliases.
|
||||
if let Some(at) = conn.rfind('@') {
|
||||
let host_part = &conn[at + 1..];
|
||||
if let Some((host, port_str)) = host_part.rsplit_once(':') {
|
||||
if let Ok(port) = port_str.parse::<u16>() {
|
||||
return (format!("{}@{}", &conn[..at], host), Some(port));
|
||||
}
|
||||
}
|
||||
} else if let Some((host, port_str)) = conn.rsplit_once(':') {
|
||||
if let Ok(port) = port_str.parse::<u16>() {
|
||||
return (host.to_string(), Some(port));
|
||||
}
|
||||
}
|
||||
(conn.to_string(), None)
|
||||
}
|
||||
|
||||
/// Extract the bare hostname from a connection string (used as a default worker name).
|
||||
pub fn hostname_from_connection(conn: &str) -> String {
|
||||
let (user_host, _port) = parse_connection(conn);
|
||||
// Strip "user@" prefix if present
|
||||
match user_host.rsplit_once('@') {
|
||||
Some((_user, host)) => host.to_string(),
|
||||
None => user_host,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the base ssh argument list for a worker (handles custom port).
|
||||
pub fn ssh_args(worker: &WorkerConfig) -> Vec<String> {
|
||||
let (user_host, port) = parse_connection(&worker.connection);
|
||||
let mut args = Vec::new();
|
||||
if let Some(p) = port {
|
||||
args.push("-p".to_string());
|
||||
args.push(p.to_string());
|
||||
}
|
||||
args.push(user_host);
|
||||
args
|
||||
}
|
||||
2
p/src/sync.rs
Normal file
2
p/src/sync.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
// Directory sync via rsync over SSH.
|
||||
// Full implementation in the run-command commit.
|
||||
Reference in New Issue
Block a user