generated from pure_sagacity/rust-starter
Compare commits
10 Commits
920f104c2e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
4285d0ae4a
|
|||
|
fac8a535de
|
|||
|
34a046fb9a
|
|||
|
c234d62d53
|
|||
|
5d5b0ae89a
|
|||
|
767fb43ee2
|
|||
|
72ee5a918b
|
|||
|
aab1d94388
|
|||
| a850f70e3b | |||
|
be860b657a
|
@@ -0,0 +1,4 @@
|
|||||||
|
# Set it to the current working directory of the harbor project
|
||||||
|
DATABASE_URL=
|
||||||
|
# Set it to the current working directory of the harbor project (it will default to /var/lib/harbor if not)
|
||||||
|
HARBOR_DATA_DIR="/Users/Maaz/Documents/Git/harbor/config/"
|
||||||
@@ -19,3 +19,5 @@ harbor.db
|
|||||||
# Environment Variables
|
# Environment Variables
|
||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
config/
|
||||||
Generated
+465
-7
@@ -245,6 +245,61 @@ version = "1.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core",
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde_core",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sha1",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -349,7 +404,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cipher 0.5.2",
|
"cipher 0.5.2",
|
||||||
"cpufeatures 0.3.0",
|
"cpufeatures 0.3.0",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -448,11 +503,11 @@ dependencies = [
|
|||||||
"colored",
|
"colored",
|
||||||
"crypto",
|
"crypto",
|
||||||
"diesel",
|
"diesel",
|
||||||
|
"dirs",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"hex",
|
"hex",
|
||||||
"keyring",
|
"keyring",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
|
||||||
"toml",
|
"toml",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
@@ -532,7 +587,7 @@ name = "crypto"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"rand",
|
"rand 0.10.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -553,7 +608,7 @@ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -600,6 +655,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -653,6 +714,27 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
@@ -773,6 +855,24 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -809,6 +909,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -823,6 +929,7 @@ checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
@@ -849,6 +956,18 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi 5.3.0",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -857,8 +976,8 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi 6.0.0",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -912,6 +1031,51 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body-util"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -921,6 +1085,41 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.65"
|
version = "0.1.65"
|
||||||
@@ -986,6 +1185,12 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.103"
|
version = "0.3.103"
|
||||||
@@ -1024,6 +1229,15 @@ version = "0.2.186"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.37.0"
|
version = "0.37.0"
|
||||||
@@ -1055,6 +1269,12 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
@@ -1070,6 +1290,12 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -1171,6 +1397,12 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1210,6 +1442,12 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -1263,6 +1501,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
@@ -1290,12 +1537,28 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r-efi"
|
name = "r-efi"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -1304,7 +1567,26 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"chacha20",
|
"chacha20",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1322,6 +1604,17 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@@ -1380,6 +1673,12 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -1458,6 +1757,30 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -1478,6 +1801,37 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "server"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -1562,6 +1916,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
@@ -1653,6 +2013,18 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
@@ -1704,12 +2076,41 @@ version = "1.1.1+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-layer"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
@@ -1735,6 +2136,22 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.1"
|
version = "1.20.1"
|
||||||
@@ -1804,6 +2221,15 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.4+wasi-0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.126"
|
version = "0.2.126"
|
||||||
@@ -1939,6 +2365,12 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.57.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zbus"
|
name = "zbus"
|
||||||
version = "5.17.0"
|
version = "5.17.0"
|
||||||
@@ -2011,12 +2443,38 @@ dependencies = [
|
|||||||
"zvariant",
|
"zvariant",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zvariant"
|
name = "zvariant"
|
||||||
version = "5.13.0"
|
version = "5.13.0"
|
||||||
|
|||||||
@@ -13,17 +13,17 @@ diesel = { version = "2.2.0", features = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
] }
|
] }
|
||||||
clap = { version = "4.6.1", features = ["derive", "cargo"] }
|
clap = { version = "4.6.1", features = ["derive", "cargo"] }
|
||||||
tokio = { version = "1.52.3", features = ["full"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
uuid = { version = "1.23.5", features = ["v4"] }
|
||||||
crypto = { path = "../crypto" }
|
crypto = { path = "../crypto" }
|
||||||
keyring = "4.1.4"
|
keyring = "4.1.4"
|
||||||
colored = "3.1.1"
|
colored = "3.1.1"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
uuid = { version = "1.23.5", features = ["v4"] }
|
|
||||||
hex = "0.4"
|
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
|
||||||
toml = "1.1.2"
|
toml = "1.1.2"
|
||||||
|
hex = "0.4"
|
||||||
|
dirs = "6"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "harbor"
|
name = "harbor"
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use std::env;
|
|
||||||
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
|
||||||
pub fn establish_connection() -> SqliteConnection {
|
pub fn establish_connection(url: String) -> SqliteConnection {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
|
||||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
let resolved_path = std::fs::canonicalize(&url)
|
||||||
SqliteConnection::establish(&database_url)
|
.map(|p| p.to_string_lossy().into_owned())
|
||||||
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
.unwrap_or(url);
|
||||||
|
|
||||||
|
SqliteConnection::establish(&resolved_path)
|
||||||
|
.unwrap_or_else(|_| panic!("Error connecting to {}", &resolved_path))
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-195
@@ -10,9 +10,10 @@ use keyring::Entry;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
mod db;
|
mod db;
|
||||||
mod store;
|
pub mod store;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Clone, Copy, PartialEq, Eq, Deserialize, diesel::AsExpression, diesel::FromSqlRow,
|
Debug, Clone, Copy, PartialEq, Eq, Deserialize, diesel::AsExpression, diesel::FromSqlRow,
|
||||||
@@ -112,6 +113,9 @@ pub enum Commands {
|
|||||||
|
|
||||||
#[arg(short = 's', long = "shell", default_value = "sh")]
|
#[arg(short = 's', long = "shell", default_value = "sh")]
|
||||||
shell: Option<String>,
|
shell: Option<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'c', long = "command")]
|
||||||
|
command: Option<String>,
|
||||||
},
|
},
|
||||||
#[command(alias = "add")]
|
#[command(alias = "add")]
|
||||||
Set {
|
Set {
|
||||||
@@ -122,9 +126,18 @@ pub enum Commands {
|
|||||||
vars: Vec<String>,
|
vars: Vec<String>,
|
||||||
},
|
},
|
||||||
Delete {
|
Delete {
|
||||||
|
#[arg(short = 'e', long = "environment")]
|
||||||
|
environment: Option<String>,
|
||||||
|
|
||||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
keys: Vec<String>,
|
keys: Vec<String>,
|
||||||
},
|
},
|
||||||
|
Show {
|
||||||
|
#[arg(short = 'e', long = "environment")]
|
||||||
|
environment: Option<String>,
|
||||||
|
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
Setup {},
|
Setup {},
|
||||||
List {},
|
List {},
|
||||||
Project {
|
Project {
|
||||||
@@ -163,30 +176,6 @@ pub fn parse_secret_pairs(raw: &[String]) -> Result<Vec<(String, String)>, Box<d
|
|||||||
Ok(pairs)
|
Ok(pairs)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_doppler_set_command(secrets: &[(String, String)]) -> String {
|
|
||||||
let mut command = String::from("doppler secrets set");
|
|
||||||
|
|
||||||
if secrets.is_empty() {
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
command.push_str(" \\");
|
|
||||||
|
|
||||||
for (index, (key, value)) in secrets.iter().enumerate() {
|
|
||||||
command.push_str("\n ");
|
|
||||||
command.push_str(key);
|
|
||||||
command.push_str("=\"");
|
|
||||||
command.push_str(value);
|
|
||||||
command.push('"');
|
|
||||||
|
|
||||||
if index + 1 != secrets.len() {
|
|
||||||
command.push_str(" \\");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
command
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum ProjectCommands {
|
pub enum ProjectCommands {
|
||||||
List {},
|
List {},
|
||||||
@@ -194,181 +183,16 @@ pub enum ProjectCommands {
|
|||||||
Delete { name: String },
|
Delete { name: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod interactions {
|
pub fn expand_tilde(path: &str) -> String {
|
||||||
use super::db::establish_connection;
|
if let Some(stripped) = path.strip_prefix("~/") {
|
||||||
use super::db::models::Project;
|
if let Some(home) = dirs::home_dir() {
|
||||||
use crate::Environment;
|
return home.join(stripped).to_string_lossy().to_string();
|
||||||
use crate::db::schema::secrets::config;
|
|
||||||
use diesel::dsl::{insert_into, update};
|
|
||||||
use diesel::result::{DatabaseErrorKind, Error as DieselError};
|
|
||||||
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper};
|
|
||||||
use std::error::Error;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
|
||||||
|
|
||||||
fn construct_error(message: &str) -> Result<()> {
|
|
||||||
Err(Box::new(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::AlreadyExists,
|
|
||||||
message.to_string(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn project_exists(proj_name: &str) -> Result<bool> {
|
|
||||||
use super::db::schema::projects::dsl::{name, projects};
|
|
||||||
let mut conn = establish_connection();
|
|
||||||
|
|
||||||
let existing_project = projects
|
|
||||||
.filter(name.eq(proj_name))
|
|
||||||
.select(Project::as_select())
|
|
||||||
.first::<Project>(&mut conn)
|
|
||||||
.optional()?;
|
|
||||||
|
|
||||||
Ok(existing_project.is_some())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_project(proj_name: &str) -> Result<()> {
|
|
||||||
use super::db::schema::projects::dsl::{created_at, id, name, projects};
|
|
||||||
let mut conn = establish_connection();
|
|
||||||
|
|
||||||
if project_exists(proj_name)? {
|
|
||||||
return construct_error("Project already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
let proj_id = Uuid::new_v4().to_string();
|
|
||||||
|
|
||||||
match insert_into(projects)
|
|
||||||
.values((
|
|
||||||
id.eq(proj_id),
|
|
||||||
name.eq(proj_name),
|
|
||||||
created_at.eq(chrono::Utc::now().naive_utc()),
|
|
||||||
))
|
|
||||||
.execute(&mut conn)
|
|
||||||
{
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => {
|
|
||||||
construct_error("Project already exists")
|
|
||||||
}
|
|
||||||
Err(err) => Err(Box::new(err)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_project(proj_name: &str) -> Result<()> {
|
path.to_string()
|
||||||
use super::db::schema::projects::dsl::{name, projects};
|
|
||||||
let mut conn = establish_connection();
|
|
||||||
|
|
||||||
if !project_exists(proj_name)? {
|
|
||||||
return construct_error("Project does not exist");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::delete(projects.filter(name.eq(proj_name))).execute(&mut conn)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_projects() -> Result<Vec<Project>> {
|
|
||||||
use super::db::schema::projects::dsl::projects;
|
|
||||||
let mut conn = establish_connection();
|
|
||||||
|
|
||||||
let results = projects
|
|
||||||
.select(Project::as_select())
|
|
||||||
.load::<Project>(&mut conn)?;
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn secret_exists() -> Result<bool> {
|
|
||||||
use super::db::schema::secrets::dsl::{id, secrets};
|
|
||||||
let mut conn = establish_connection();
|
|
||||||
|
|
||||||
let existing_secret = secrets.select(id).first::<String>(&mut conn).optional()?;
|
|
||||||
|
|
||||||
Ok(existing_secret.is_some())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_project_id(proj_name: &str) -> Result<String> {
|
|
||||||
use super::db::schema::projects::dsl::{id, name, projects};
|
|
||||||
let mut conn = establish_connection();
|
|
||||||
|
|
||||||
let project_id = projects
|
|
||||||
.filter(name.eq(proj_name))
|
|
||||||
.select(id)
|
|
||||||
.first::<String>(&mut conn)
|
|
||||||
.optional()?;
|
|
||||||
|
|
||||||
match project_id {
|
|
||||||
Some(pid) => Ok(pid),
|
|
||||||
None => Err(Box::new(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::NotFound,
|
|
||||||
"Project not found",
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_secret(
|
|
||||||
proj_id: &str,
|
|
||||||
secret_name: &str,
|
|
||||||
secret_value: Vec<u8>,
|
|
||||||
conf: Environment,
|
|
||||||
non: crypto::Nonce,
|
|
||||||
) -> Result<()> {
|
|
||||||
use super::db::schema::secrets::dsl::{
|
|
||||||
config, created_at, name, nonce, project_id, secret, secrets,
|
|
||||||
};
|
|
||||||
let mut conn = establish_connection();
|
|
||||||
|
|
||||||
if secret_exists()? {
|
|
||||||
update(secrets.filter(name.eq(secret_name)))
|
|
||||||
.set(secret.eq(secret_value))
|
|
||||||
.execute(&mut conn)?;
|
|
||||||
} else {
|
|
||||||
insert_into(secrets)
|
|
||||||
.values((
|
|
||||||
name.eq(secret_name),
|
|
||||||
secret.eq(secret_value),
|
|
||||||
project_id.eq(proj_id),
|
|
||||||
config.eq(conf),
|
|
||||||
nonce.eq(non.to_vec()),
|
|
||||||
created_at.eq(chrono::Utc::now().naive_utc()),
|
|
||||||
))
|
|
||||||
.execute(&mut conn)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_project_secrets(
|
|
||||||
proj_id: &str,
|
|
||||||
environment: Environment,
|
|
||||||
) -> Result<Vec<(String, Vec<u8>, Vec<u8>)>> {
|
|
||||||
use super::db::schema::secrets::dsl::{config, name, nonce, project_id, secret, secrets};
|
|
||||||
let mut conn = establish_connection();
|
|
||||||
|
|
||||||
let results = secrets
|
|
||||||
.filter(project_id.eq(proj_id))
|
|
||||||
.filter(config.eq(environment))
|
|
||||||
.select((name, secret, nonce))
|
|
||||||
.load::<(String, Vec<u8>, Vec<u8>)>(&mut conn)?;
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_secret(secret_name: &str) -> Result<()> {
|
|
||||||
use super::db::schema::secrets::dsl::{name, secrets};
|
|
||||||
let mut conn = establish_connection();
|
|
||||||
|
|
||||||
if !secret_exists()? {
|
|
||||||
return Err(Box::new(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::NotFound,
|
|
||||||
"Secret does not exist",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::delete(secrets.filter(name.eq(secret_name))).execute(&mut conn)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> {
|
pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> {
|
||||||
let entry = Entry::new("harbor", "encryption-key")?;
|
let entry = Entry::new("harbor", "encryption-key")?;
|
||||||
|
|
||||||
|
|||||||
+268
-93
@@ -6,18 +6,20 @@ use clap::crate_version;
|
|||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
use cli::Environment;
|
use cli::Environment;
|
||||||
use cli::config::{Config, ConfigError};
|
use cli::config::{Config, ConfigError};
|
||||||
|
use cli::expand_tilde;
|
||||||
use cli::gen_or_get_key;
|
use cli::gen_or_get_key;
|
||||||
use cli::get_input;
|
use cli::get_input;
|
||||||
use cli::interactions::delete_secret;
|
use cli::store::Store;
|
||||||
use cli::interactions::get_project_id;
|
use cli::store::sqlite::SqliteStore;
|
||||||
use cli::interactions::get_project_secrets;
|
|
||||||
use cli::interactions::get_projects;
|
|
||||||
use cli::interactions::set_secret;
|
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use crypto::encrypt;
|
use crypto::encrypt;
|
||||||
|
|
||||||
const GIT_VERSION: &str = env!("GIT_VERSION");
|
const GIT_VERSION: &str = env!("GIT_VERSION");
|
||||||
|
|
||||||
|
fn print_error(message: impl std::fmt::Display) {
|
||||||
|
eprintln!("{}", message.to_string().bright_red().bold());
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
use cli::{Commands, ProjectCommands};
|
use cli::{Commands, ProjectCommands};
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -37,37 +39,103 @@ fn main() {
|
|||||||
let root = match std::env::current_dir() {
|
let root = match std::env::current_dir() {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error resolving current directory: {}", err);
|
print_error(format!("Error resolving current directory: {}", err));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let db_path = expand_tilde("~/.local/share/harbor/database.db");
|
||||||
let config_path = root.join(".harbor.toml");
|
let config_path = root.join(".harbor.toml");
|
||||||
let has_config = config_path.exists();
|
let has_config = config_path.exists();
|
||||||
|
let store = SqliteStore::new(db_path);
|
||||||
if !has_config {
|
|
||||||
eprintln!(
|
|
||||||
"{}",
|
|
||||||
"Warning: .harbor.toml not found. Some commands are disabled.".yellow()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Some(c) => match c {
|
Some(c) => match c {
|
||||||
_ if requires_config(&c) && !has_config => {
|
_ if requires_config(&c) && !has_config => {
|
||||||
eprintln!(
|
print_error("Missing .harbor.toml. Run `harbor config create` first.");
|
||||||
"{}",
|
|
||||||
"Missing .harbor.toml. Run `harbor config create` first.".yellow()
|
|
||||||
);
|
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
Commands::Show { environment, key } => {
|
||||||
|
let config = require_config(&root);
|
||||||
|
let environment: Environment = match environment {
|
||||||
|
Some(env) => match env.parse::<Environment>() {
|
||||||
|
Ok(parsed) => parsed,
|
||||||
|
Err(_) => {
|
||||||
|
print_error(format!("Invalid environment '{}'.", env));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => config.default_env.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = match store.get_project_id(&config.name) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(format!(
|
||||||
|
"Unable to get project ID for '{}': {}",
|
||||||
|
config.name, e
|
||||||
|
));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let secrets = match store.get_project_secrets(&project, environment) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(format!("Error getting secrets: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret = secrets.into_iter().find(|(name, _, _)| name == &key);
|
||||||
|
|
||||||
|
match secret {
|
||||||
|
Some((_, ciphertext, nonce)) => {
|
||||||
|
let key = match gen_or_get_key() {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(_) => {
|
||||||
|
print_error("Error generating or getting key");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let nonce = crypto::Nonce::from_slice(&nonce);
|
||||||
|
let decrypted = match crypto::decrypt(&key, ciphertext, nonce) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(format!("Error decrypting secret: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let decrypted_str = match String::from_utf8(decrypted) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(format!(
|
||||||
|
"Error converting decrypted secret to string: {}",
|
||||||
|
e
|
||||||
|
));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{}", decrypted_str);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
print_error(format!(
|
||||||
|
"Secret with key '{}' not found in project '{}'.",
|
||||||
|
key, config.name
|
||||||
|
));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Commands::Set { environment, vars } => {
|
Commands::Set { environment, vars } => {
|
||||||
let config = require_config(&root);
|
let config = require_config(&root);
|
||||||
let environment: Environment = match environment {
|
let environment: Environment = match environment {
|
||||||
Some(env) => match env.parse::<Environment>() {
|
Some(env) => match env.parse::<Environment>() {
|
||||||
Ok(parsed) => parsed,
|
Ok(parsed) => parsed,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!("Invalid environment '{}'.", env);
|
print_error(format!("Invalid environment '{}'.", env));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -77,49 +145,126 @@ fn main() {
|
|||||||
let pairs = match cli::parse_secret_pairs(&vars) {
|
let pairs = match cli::parse_secret_pairs(&vars) {
|
||||||
Ok(pairs) => pairs,
|
Ok(pairs) => pairs,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error parsing secrets: {}", e);
|
print_error(format!("Error parsing secrets: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if pairs.is_empty() {
|
if pairs.is_empty() {
|
||||||
eprintln!("No secrets provided. Use KEY=VALUE pairs.");
|
print_error("No secrets provided. Use KEY=VALUE pairs.");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let project = match get_project_id(&config.name) {
|
let project = match store.get_project_id(&config.name) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
|
print_error(format!(
|
||||||
|
"Unable to get project ID for '{}': {}",
|
||||||
|
config.name, e
|
||||||
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for pair in pairs {
|
for pair in pairs {
|
||||||
|
let existed = match store.secret_exists(&project, &pair.0, environment) {
|
||||||
|
Ok(exists) => exists,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(format!(
|
||||||
|
"Error checking existing secret for key '{}': {}",
|
||||||
|
pair.0, e
|
||||||
|
));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if existed {
|
||||||
|
let confirmation = match get_input(
|
||||||
|
format!("Secret '{}' already exists. Overwrite? (y/N)", pair.0)
|
||||||
|
.yellow(),
|
||||||
|
':',
|
||||||
|
false,
|
||||||
|
) {
|
||||||
|
Ok(input) => input,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(format!("Error getting confirmation: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if confirmation.to_lowercase() != "y" {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!("Skipped secret '{}' in {}.", pair.0, environment.as_str())
|
||||||
|
.dimmed()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let key = match gen_or_get_key() {
|
let key = match gen_or_get_key() {
|
||||||
Ok(k) => k,
|
Ok(k) => k,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!("Error generating or getting key");
|
print_error("Error generating or getting key");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (nonce, encrypted) = match encrypt(&key, pair.1.as_bytes().to_vec()) {
|
let (nonce, encrypted) = match encrypt(&key, pair.1.as_bytes().to_vec()) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error encrypting secret for key '{}': {}", pair.0, e);
|
print_error(format!(
|
||||||
|
"Error encrypting secret for key '{}': {}",
|
||||||
|
pair.0, e
|
||||||
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match set_secret(&project, &pair.0, encrypted, environment, nonce) {
|
match store.set_secret(&project, &pair.0, encrypted, environment, nonce) {
|
||||||
Ok(()) => {}
|
Ok(()) => {
|
||||||
|
let verb = if existed { "Updated" } else { "Set" };
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"{} secret '{}' in {}.",
|
||||||
|
verb,
|
||||||
|
pair.0,
|
||||||
|
environment.as_str()
|
||||||
|
)
|
||||||
|
.bright_green()
|
||||||
|
.bold()
|
||||||
|
);
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error setting secret for key '{}': {}", pair.0, e);
|
print_error(format!(
|
||||||
|
"Error setting secret for key '{}': {}",
|
||||||
|
pair.0, e
|
||||||
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Delete { keys } => {
|
Commands::Delete { environment, keys } => {
|
||||||
|
let config = require_config(&root);
|
||||||
|
let environment: Environment = match environment {
|
||||||
|
Some(env) => match env.parse::<Environment>() {
|
||||||
|
Ok(parsed) => parsed,
|
||||||
|
Err(_) => {
|
||||||
|
print_error(format!("Invalid environment '{}'.", env));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => config.default_env.into(),
|
||||||
|
};
|
||||||
|
let proj_id = match store.get_project_id(&config.name) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(format!(
|
||||||
|
"Unable to get project ID for '{}': {}",
|
||||||
|
config.name, e
|
||||||
|
));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut cleaned = Vec::new();
|
let mut cleaned = Vec::new();
|
||||||
|
|
||||||
for key in keys {
|
for key in keys {
|
||||||
@@ -133,17 +278,17 @@ fn main() {
|
|||||||
let cleaned = cleaned;
|
let cleaned = cleaned;
|
||||||
|
|
||||||
if cleaned.is_empty() {
|
if cleaned.is_empty() {
|
||||||
eprintln!("No secret keys provided. Use one or more keys.");
|
print_error("No secret keys provided. Use one or more keys.");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
for key in cleaned {
|
for key in cleaned {
|
||||||
match delete_secret(&key) {
|
match store.delete_secret(&proj_id, &key, environment) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
println!("Deleted secret for key '{}'", key);
|
println!("Deleted secret for key '{}'", key);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error deleting secret for key '{}': {}", key, e);
|
print_error(format!("Error deleting secret for key '{}': {}", key, e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -155,16 +300,19 @@ fn main() {
|
|||||||
Some(env) => match env.parse::<Environment>() {
|
Some(env) => match env.parse::<Environment>() {
|
||||||
Ok(parsed) => parsed,
|
Ok(parsed) => parsed,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!("Invalid environment '{}'.", env);
|
print_error(format!("Invalid environment '{}'.", env));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => config.default_env.into(),
|
None => config.default_env.into(),
|
||||||
};
|
};
|
||||||
let proj_id = match get_project_id(&config.name) {
|
let proj_id = match store.get_project_id(&config.name) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
|
print_error(format!(
|
||||||
|
"Unable to get project ID for '{}': {}",
|
||||||
|
config.name, e
|
||||||
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -178,10 +326,10 @@ fn main() {
|
|||||||
cmd.args(&after[1..]);
|
cmd.args(&after[1..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let secrets = match get_project_secrets(&proj_id, environment) {
|
let secrets = match store.get_project_secrets(&proj_id, environment) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error getting secrets: {}", e);
|
print_error(format!("Error getting secrets: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -192,7 +340,7 @@ fn main() {
|
|||||||
let key = match gen_or_get_key() {
|
let key = match gen_or_get_key() {
|
||||||
Ok(k) => k,
|
Ok(k) => k,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!("Error generating or getting key");
|
print_error("Error generating or getting key");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -200,7 +348,10 @@ fn main() {
|
|||||||
let decrypted = match crypto::decrypt(&key, ciphertext, nonce) {
|
let decrypted = match crypto::decrypt(&key, ciphertext, nonce) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error decrypting secret for key '{}': {}", name, e);
|
print_error(format!(
|
||||||
|
"Error decrypting secret for key '{}': {}",
|
||||||
|
name, e
|
||||||
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -208,10 +359,10 @@ fn main() {
|
|||||||
let decrypted_str = match String::from_utf8(decrypted) {
|
let decrypted_str = match String::from_utf8(decrypted) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
print_error(format!(
|
||||||
"Error converting decrypted secret to string for key '{}': {}",
|
"Error converting decrypted secret to string for key '{}': {}",
|
||||||
name, e
|
name, e
|
||||||
);
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -223,13 +374,8 @@ fn main() {
|
|||||||
cmd.env(key, value);
|
cmd.env(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.spawn().is_err() {
|
|
||||||
eprintln!("Error executing command: {:?}", after);
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.status().is_err() {
|
if cmd.status().is_err() {
|
||||||
eprintln!("Error waiting for command to finish: {:?}", after);
|
print_error(format!("Error waiting for command to finish: {:?}", after));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
} else {
|
} else {
|
||||||
process::exit(0);
|
process::exit(0);
|
||||||
@@ -238,18 +384,21 @@ fn main() {
|
|||||||
Commands::List {} => {
|
Commands::List {} => {
|
||||||
// Get all secrets, then use colorize and split them between environments, and print them out in a table format.
|
// Get all secrets, then use colorize and split them between environments, and print them out in a table format.
|
||||||
let config = require_config(&root);
|
let config = require_config(&root);
|
||||||
let proj_id = match get_project_id(&config.name) {
|
let proj_id = match store.get_project_id(&config.name) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
|
print_error(format!(
|
||||||
|
"Unable to get project ID for '{}': {}",
|
||||||
|
config.name, e
|
||||||
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let secrets = match get_project_secrets(&proj_id, Environment::Dev) {
|
let secrets = match store.get_project_secrets(&proj_id, Environment::Dev) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error getting secrets: {}", e);
|
print_error(format!("Error getting secrets: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -272,10 +421,10 @@ fn main() {
|
|||||||
}
|
}
|
||||||
Commands::Project { command } => match command {
|
Commands::Project { command } => match command {
|
||||||
ProjectCommands::List {} => {
|
ProjectCommands::List {} => {
|
||||||
let projects = match get_projects() {
|
let projects = match store.get_projects() {
|
||||||
Ok(projects) => projects,
|
Ok(projects) => projects,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error getting projects: {}", e);
|
print_error(format!("Error getting projects: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -287,31 +436,33 @@ fn main() {
|
|||||||
let project_name = match get_input("Project name", ':', false) {
|
let project_name = match get_input("Project name", ':', false) {
|
||||||
Ok(name) => name,
|
Ok(name) => name,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error getting project name: {}", e);
|
print_error(format!("Error getting project name: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match cli::interactions::create_project(&project_name) {
|
match store.create_project(&project_name) {
|
||||||
Ok(()) => println!("{}", "Project created successfully.".cyan()),
|
Ok(()) => {
|
||||||
|
println!("{}", "Project created successfully.".bright_green().bold())
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error creating project: {}", e);
|
print_error(format!("Error creating project: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProjectCommands::Delete { name } => {
|
ProjectCommands::Delete { name } => {
|
||||||
// We'll check if the project exists before prompting for confirmation
|
// We'll check if the project exists before prompting for confirmation
|
||||||
let exists = match cli::interactions::project_exists(&name) {
|
let exists = match store.project_exists(&name) {
|
||||||
Ok(exists) => exists,
|
Ok(exists) => exists,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error checking if project exists: {}", e);
|
print_error(format!("Error checking if project exists: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
eprintln!("Project '{}' does not exist.", name);
|
print_error(format!("Project '{}' does not exist.", name));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,18 +478,21 @@ fn main() {
|
|||||||
) {
|
) {
|
||||||
Ok(input) => input,
|
Ok(input) => input,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error getting confirmation: {}", e);
|
print_error(format!("Error getting confirmation: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if confirmation.to_lowercase() == "y" {
|
if confirmation.to_lowercase() == "y" {
|
||||||
match cli::interactions::delete_project(&name) {
|
match store.delete_project(&name) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
println!("{}", "Project deleted successfully.".cyan())
|
println!(
|
||||||
|
"{}",
|
||||||
|
"Project deleted successfully.".bright_green().bold()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error deleting project: {}", e);
|
print_error(format!("Error deleting project: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,22 +501,29 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Commands::Shell { environment, shell } => {
|
Commands::Shell {
|
||||||
|
environment,
|
||||||
|
shell,
|
||||||
|
command,
|
||||||
|
} => {
|
||||||
let config = require_config(&root);
|
let config = require_config(&root);
|
||||||
let environment: Environment = match environment {
|
let environment: Environment = match environment {
|
||||||
Some(env) => match env.parse::<Environment>() {
|
Some(env) => match env.parse::<Environment>() {
|
||||||
Ok(parsed) => parsed,
|
Ok(parsed) => parsed,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!("Invalid environment '{}'.", env);
|
print_error(format!("Invalid environment '{}'.", env));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => config.default_env.into(),
|
None => config.default_env.into(),
|
||||||
};
|
};
|
||||||
let proj_id = match get_project_id(&config.name) {
|
let proj_id = match store.get_project_id(&config.name) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
|
print_error(format!(
|
||||||
|
"Unable to get project ID for '{}': {}",
|
||||||
|
config.name, e
|
||||||
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -372,15 +533,20 @@ fn main() {
|
|||||||
|
|
||||||
let shell = shell
|
let shell = shell
|
||||||
.or_else(|| std::env::var("SHELL").ok())
|
.or_else(|| std::env::var("SHELL").ok())
|
||||||
.or_else(|| Some("sh".into()))
|
.or_else(|| Some("/bin/sh".into()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut cmd = process::Command::new(&shell);
|
let mut cmd = process::Command::new(&shell);
|
||||||
|
|
||||||
let secrets = match get_project_secrets(&proj_id, environment) {
|
if let Some(command) = command {
|
||||||
|
cmd.arg("-c");
|
||||||
|
cmd.arg(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
let secrets = match store.get_project_secrets(&proj_id, environment) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error getting secrets: {}", e);
|
print_error(format!("Error getting secrets: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -391,7 +557,7 @@ fn main() {
|
|||||||
let key = match gen_or_get_key() {
|
let key = match gen_or_get_key() {
|
||||||
Ok(k) => k,
|
Ok(k) => k,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!("Error generating or getting key");
|
print_error("Error generating or getting key");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -399,7 +565,10 @@ fn main() {
|
|||||||
let decrypted = match crypto::decrypt(&key, ciphertext, nonce) {
|
let decrypted = match crypto::decrypt(&key, ciphertext, nonce) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error decrypting secret for key '{}': {}", name, e);
|
print_error(format!(
|
||||||
|
"Error decrypting secret for key '{}': {}",
|
||||||
|
name, e
|
||||||
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -407,10 +576,10 @@ fn main() {
|
|||||||
let decrypted_str = match String::from_utf8(decrypted) {
|
let decrypted_str = match String::from_utf8(decrypted) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
print_error(format!(
|
||||||
"Error converting decrypted secret to string for key '{}': {}",
|
"Error converting decrypted secret to string for key '{}': {}",
|
||||||
name, e
|
name, e
|
||||||
);
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -422,13 +591,16 @@ fn main() {
|
|||||||
cmd.env(key, value);
|
cmd.env(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.spawn().is_err() {
|
let mut child = match cmd.spawn() {
|
||||||
eprintln!("Error executing command: {:?}", &shell);
|
Ok(c) => c,
|
||||||
|
Err(_) => {
|
||||||
|
print_error(format!("Error executing command: {:?}", &shell));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if cmd.status().is_err() {
|
if child.wait().is_err() {
|
||||||
eprintln!("Error waiting for command to finish: {:?}", &shell);
|
print_error(format!("Error waiting for command to finish: {:?}", &shell));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
} else {
|
} else {
|
||||||
process::exit(0);
|
process::exit(0);
|
||||||
@@ -438,16 +610,16 @@ fn main() {
|
|||||||
use std::process;
|
use std::process;
|
||||||
// Setup will get all projects and prompt the user to select one,
|
// Setup will get all projects and prompt the user to select one,
|
||||||
// then create a .harbor.toml file in the current directory with the selected project name.
|
// then create a .harbor.toml file in the current directory with the selected project name.
|
||||||
let projects = match get_projects() {
|
let projects = match store.get_projects() {
|
||||||
Ok(projects) => projects,
|
Ok(projects) => projects,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error getting projects: {}", e);
|
print_error(format!("Error getting projects: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if projects.is_empty() {
|
if projects.is_empty() {
|
||||||
eprintln!("No projects found. Please create a project first.");
|
print_error("No projects found. Please create a project first.");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +633,7 @@ fn main() {
|
|||||||
{
|
{
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error spawning fzf: {}", e);
|
print_error(format!("Error spawning fzf: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -478,20 +650,20 @@ fn main() {
|
|||||||
let output = match fzf.wait_with_output() {
|
let output = match fzf.wait_with_output() {
|
||||||
Ok(o) => o,
|
Ok(o) => o,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error waiting for fzf: {}", e);
|
print_error(format!("Error waiting for fzf: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
eprintln!("fzf exited with non-zero status");
|
print_error("fzf exited with non-zero status");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let project = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let project = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
|
||||||
if project.is_empty() {
|
if project.is_empty() {
|
||||||
eprintln!("No project selected.");
|
print_error("No project selected.");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,13 +677,18 @@ config = "dev""#,
|
|||||||
let config_path = root.join(".harbor.toml");
|
let config_path = root.join(".harbor.toml");
|
||||||
match std::fs::write(&config_path, new_config) {
|
match std::fs::write(&config_path, new_config) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("Created .harbor.toml for project '{}'.", project);
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!("Created .harbor.toml for project '{}'.", project)
|
||||||
|
.bright_green()
|
||||||
|
.bold()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
print_error(format!(
|
||||||
"Error creating .harbor.toml for project '{}': {}",
|
"Error creating .harbor.toml for project '{}': {}",
|
||||||
project, e
|
project, e
|
||||||
);
|
));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -532,14 +709,11 @@ fn require_config(root: &std::path::Path) -> Config {
|
|||||||
match Config::from_repo_root(root) {
|
match Config::from_repo_root(root) {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(ConfigError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
|
Err(ConfigError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
eprintln!(
|
print_error("Missing .harbor.toml. Run `harbor config create` first.");
|
||||||
"{}",
|
|
||||||
"Missing .harbor.toml. Run `harbor config create` first.".yellow()
|
|
||||||
);
|
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error reading config: {}", err);
|
print_error(format!("Error reading config: {}", err));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,6 +723,7 @@ fn requires_config(command: &cli::Commands) -> bool {
|
|||||||
matches!(
|
matches!(
|
||||||
command,
|
command,
|
||||||
cli::Commands::Inject { .. }
|
cli::Commands::Inject { .. }
|
||||||
|
| cli::Commands::Show { .. }
|
||||||
| cli::Commands::Set { .. }
|
| cli::Commands::Set { .. }
|
||||||
| cli::Commands::Delete { .. }
|
| cli::Commands::Delete { .. }
|
||||||
| cli::Commands::Shell { .. }
|
| cli::Commands::Shell { .. }
|
||||||
|
|||||||
@@ -3,5 +3,35 @@
|
|||||||
// With this trait, we can implement other savers like Postgres, MySQL, etc.
|
// With this trait, we can implement other savers like Postgres, MySQL, etc.
|
||||||
// Or even third party services like Hashicorp Vault, AWS Secrets Manager, etc.
|
// Or even third party services like Hashicorp Vault, AWS Secrets Manager, etc.
|
||||||
|
|
||||||
// For now, I'll just get an MVP in place.
|
use std::error::Error;
|
||||||
pub trait Store {}
|
|
||||||
|
pub mod sqlite;
|
||||||
|
|
||||||
|
use crate::{Environment, db::models::Project};
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||||
|
|
||||||
|
pub trait Store {
|
||||||
|
fn project_exists(&self, proj_name: &str) -> Result<bool>;
|
||||||
|
fn create_project(&self, proj_name: &str) -> Result<()>;
|
||||||
|
fn delete_project(&self, proj_name: &str) -> Result<()>;
|
||||||
|
fn get_projects(&self) -> Result<Vec<Project>>;
|
||||||
|
fn get_project_id(&self, proj_name: &str) -> Result<String>;
|
||||||
|
fn secret_exists(&self, proj_id: &str, secret_name: &str, environment: Environment)
|
||||||
|
-> Result<bool>;
|
||||||
|
fn set_secret(
|
||||||
|
&self,
|
||||||
|
proj_id: &str,
|
||||||
|
secret_name: &str,
|
||||||
|
secret_value: Vec<u8>,
|
||||||
|
conf: Environment,
|
||||||
|
non: crypto::Nonce,
|
||||||
|
) -> Result<()>;
|
||||||
|
fn get_project_secrets(
|
||||||
|
&self,
|
||||||
|
proj_id: &str,
|
||||||
|
environment: Environment,
|
||||||
|
) -> Result<Vec<(String, Vec<u8>, Vec<u8>)>>;
|
||||||
|
fn delete_secret(&self, proj_id: &str, secret_name: &str, environment: Environment)
|
||||||
|
-> Result<()>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
use crate::db::models::Project;
|
||||||
|
use crate::store::Store;
|
||||||
|
use diesel::dsl::{insert_into, update};
|
||||||
|
use diesel::result::{DatabaseErrorKind, Error as DieselError};
|
||||||
|
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct SqliteStore {
|
||||||
|
database_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqliteStore {
|
||||||
|
pub fn new(database_url: String) -> Self {
|
||||||
|
SqliteStore { database_url }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_url(&self) -> String {
|
||||||
|
if self.database_url.is_empty() {
|
||||||
|
std::env::var("DATABASE_URL").expect("NO DATABASE URL WAS SET")
|
||||||
|
} else {
|
||||||
|
self.database_url.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn construct_error(message: &str) -> super::Result<()> {
|
||||||
|
Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::AlreadyExists,
|
||||||
|
message.to_string(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store for SqliteStore {
|
||||||
|
fn create_project(&self, proj_name: &str) -> super::Result<()> {
|
||||||
|
use crate::db::schema::projects::dsl::{created_at, id, name, projects};
|
||||||
|
let mut conn = crate::db::establish_connection(self.db_url());
|
||||||
|
|
||||||
|
if self.project_exists(proj_name)? {
|
||||||
|
return construct_error("Project already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
let proj_id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
match insert_into(projects)
|
||||||
|
.values((
|
||||||
|
id.eq(proj_id),
|
||||||
|
name.eq(proj_name),
|
||||||
|
created_at.eq(chrono::Utc::now().naive_utc()),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => {
|
||||||
|
construct_error("Project already exists")
|
||||||
|
}
|
||||||
|
Err(err) => Err(Box::new(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_project(&self, proj_name: &str) -> super::Result<()> {
|
||||||
|
use crate::db::schema::projects::dsl::{name, projects};
|
||||||
|
let mut conn = crate::db::establish_connection(self.db_url());
|
||||||
|
|
||||||
|
if !self.project_exists(proj_name)? {
|
||||||
|
return construct_error("Project does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::delete(projects.filter(name.eq(proj_name))).execute(&mut conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn delete_secret(
|
||||||
|
&self,
|
||||||
|
proj_id: &str,
|
||||||
|
secret_name: &str,
|
||||||
|
environment: crate::Environment,
|
||||||
|
) -> super::Result<()> {
|
||||||
|
use crate::db::schema::secrets::dsl::{config, id, name, project_id, secrets};
|
||||||
|
let mut conn = crate::db::establish_connection(self.db_url());
|
||||||
|
|
||||||
|
let existing_secret = secrets
|
||||||
|
.filter(project_id.eq(proj_id))
|
||||||
|
.filter(name.eq(secret_name))
|
||||||
|
.filter(config.eq(environment))
|
||||||
|
.select(id)
|
||||||
|
.first::<String>(&mut conn)
|
||||||
|
.optional()?;
|
||||||
|
|
||||||
|
if existing_secret.is_none() {
|
||||||
|
return Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
"Secret does not exist",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::delete(
|
||||||
|
secrets
|
||||||
|
.filter(project_id.eq(proj_id))
|
||||||
|
.filter(name.eq(secret_name))
|
||||||
|
.filter(config.eq(environment)),
|
||||||
|
)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn get_project_id(&self, proj_name: &str) -> super::Result<String> {
|
||||||
|
use crate::db::schema::projects::dsl::{id, name, projects};
|
||||||
|
let mut conn = crate::db::establish_connection(self.db_url());
|
||||||
|
|
||||||
|
let project_id = projects
|
||||||
|
.filter(name.eq(proj_name))
|
||||||
|
.select(id)
|
||||||
|
.first::<String>(&mut conn)
|
||||||
|
.optional()?;
|
||||||
|
|
||||||
|
match project_id {
|
||||||
|
Some(pid) => Ok(pid),
|
||||||
|
None => Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
"Project not found",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_project_secrets(
|
||||||
|
&self,
|
||||||
|
proj_id: &str,
|
||||||
|
environment: crate::Environment,
|
||||||
|
) -> super::Result<Vec<(String, Vec<u8>, Vec<u8>)>> {
|
||||||
|
use crate::db::schema::secrets::dsl::{config, name, nonce, project_id, secret, secrets};
|
||||||
|
let mut conn = crate::db::establish_connection(self.db_url());
|
||||||
|
|
||||||
|
let results = secrets
|
||||||
|
.filter(project_id.eq(proj_id))
|
||||||
|
.filter(config.eq(environment))
|
||||||
|
.select((name, secret, nonce))
|
||||||
|
.load::<(String, Vec<u8>, Vec<u8>)>(&mut conn)?;
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
fn get_projects(&self) -> super::Result<Vec<Project>> {
|
||||||
|
use crate::db::schema::projects::dsl::projects;
|
||||||
|
let mut conn = crate::db::establish_connection(self.db_url());
|
||||||
|
|
||||||
|
let results = projects
|
||||||
|
.select(Project::as_select())
|
||||||
|
.load::<Project>(&mut conn)?;
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_exists(&self, proj_name: &str) -> super::Result<bool> {
|
||||||
|
use crate::db::schema::projects::dsl::{name, projects};
|
||||||
|
let mut conn = crate::db::establish_connection(self.db_url());
|
||||||
|
|
||||||
|
let existing_project = projects
|
||||||
|
.filter(name.eq(proj_name))
|
||||||
|
.select(Project::as_select())
|
||||||
|
.first::<Project>(&mut conn)
|
||||||
|
.optional()?;
|
||||||
|
|
||||||
|
Ok(existing_project.is_some())
|
||||||
|
}
|
||||||
|
fn secret_exists(
|
||||||
|
&self,
|
||||||
|
proj_id: &str,
|
||||||
|
secret_name: &str,
|
||||||
|
environment: crate::Environment,
|
||||||
|
) -> super::Result<bool> {
|
||||||
|
use crate::db::schema::secrets::dsl::{config, id, name, project_id, secrets};
|
||||||
|
let mut conn = crate::db::establish_connection(self.db_url());
|
||||||
|
|
||||||
|
let existing_secret = secrets
|
||||||
|
.filter(project_id.eq(proj_id))
|
||||||
|
.filter(name.eq(secret_name))
|
||||||
|
.filter(config.eq(environment))
|
||||||
|
.select(id)
|
||||||
|
.first::<String>(&mut conn)
|
||||||
|
.optional()?;
|
||||||
|
|
||||||
|
Ok(existing_secret.is_some())
|
||||||
|
}
|
||||||
|
fn set_secret(
|
||||||
|
&self,
|
||||||
|
proj_id: &str,
|
||||||
|
secret_name: &str,
|
||||||
|
secret_value: Vec<u8>,
|
||||||
|
conf: crate::Environment,
|
||||||
|
non: crypto::Nonce,
|
||||||
|
) -> super::Result<()> {
|
||||||
|
use crate::db::schema::secrets::dsl::{
|
||||||
|
config, created_at, id, name, nonce, project_id, secret, secrets,
|
||||||
|
};
|
||||||
|
let mut conn = crate::db::establish_connection(self.db_url());
|
||||||
|
|
||||||
|
if self.secret_exists(proj_id, secret_name, conf)? {
|
||||||
|
update(
|
||||||
|
secrets
|
||||||
|
.filter(project_id.eq(proj_id))
|
||||||
|
.filter(name.eq(secret_name))
|
||||||
|
.filter(config.eq(conf)),
|
||||||
|
)
|
||||||
|
.set((secret.eq(secret_value), nonce.eq(non.to_vec())))
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
} else {
|
||||||
|
insert_into(secrets)
|
||||||
|
.values((
|
||||||
|
id.eq(Uuid::new_v4().to_string()),
|
||||||
|
name.eq(secret_name),
|
||||||
|
secret.eq(secret_value),
|
||||||
|
project_id.eq(proj_id),
|
||||||
|
config.eq(conf),
|
||||||
|
nonce.eq(non.to_vec()),
|
||||||
|
created_at.eq(chrono::Utc::now().naive_utc()),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
use cli::Environment;
|
||||||
|
use cli::config::Config;
|
||||||
|
use cli::parse_secret_pairs;
|
||||||
|
use cli::store::Store;
|
||||||
|
use cli::store::sqlite::SqliteStore;
|
||||||
|
use diesel::connection::SimpleConnection;
|
||||||
|
use diesel::{Connection, SqliteConnection};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn test_lock() -> &'static Mutex<()> {
|
||||||
|
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||||
|
LOCK.get_or_init(|| Mutex::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_test_db() -> PathBuf {
|
||||||
|
let db_path = std::env::temp_dir().join(format!("harbor_test_{}.db", Uuid::new_v4()));
|
||||||
|
std::fs::File::create(&db_path).expect("create sqlite file");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("DATABASE_URL", &db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let database_url = db_path.to_str().expect("db path");
|
||||||
|
let mut conn = SqliteConnection::establish(database_url).expect("connect sqlite");
|
||||||
|
conn.batch_execute(
|
||||||
|
"PRAGMA foreign_keys = ON;\
|
||||||
|
CREATE TABLE projects (\
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,\
|
||||||
|
name TEXT NOT NULL UNIQUE,\
|
||||||
|
created_at TIMESTAMP NOT NULL\
|
||||||
|
);\
|
||||||
|
CREATE TABLE secrets (\
|
||||||
|
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(16)))),\
|
||||||
|
name TEXT NOT NULL,\
|
||||||
|
project_id TEXT NOT NULL,\
|
||||||
|
config TEXT NOT NULL,\
|
||||||
|
secret BLOB NOT NULL,\
|
||||||
|
nonce BLOB NOT NULL,\
|
||||||
|
created_at TIMESTAMP NOT NULL,\
|
||||||
|
FOREIGN KEY(project_id) REFERENCES projects(id)\
|
||||||
|
);",
|
||||||
|
)
|
||||||
|
.expect("create schema");
|
||||||
|
|
||||||
|
db_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_test_db(path: PathBuf) {
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn environment_parsing_and_display() {
|
||||||
|
let dev: Environment = "DEV".parse().expect("parse dev");
|
||||||
|
let staging: Environment = " staging ".parse().expect("parse staging");
|
||||||
|
|
||||||
|
assert_eq!(dev, Environment::Dev);
|
||||||
|
assert_eq!(staging, Environment::Staging);
|
||||||
|
assert_eq!(format!("{}", Environment::Prod), "prod");
|
||||||
|
|
||||||
|
let err = "unknown".parse::<Environment>().unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"Environment must be one of: dev, prod, staging"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_secret_pairs_handles_valid_and_invalid_input() {
|
||||||
|
let input = vec![
|
||||||
|
" FOO=bar ".to_string(),
|
||||||
|
"".to_string(),
|
||||||
|
"BAZ=qux".to_string(),
|
||||||
|
"A=B=C".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let parsed = parse_secret_pairs(&input).expect("parse secret pairs");
|
||||||
|
assert_eq!(parsed.len(), 3);
|
||||||
|
assert_eq!(parsed[0], ("FOO".to_string(), "bar".to_string()));
|
||||||
|
assert_eq!(parsed[1], ("BAZ".to_string(), "qux".to_string()));
|
||||||
|
assert_eq!(parsed[2], ("A".to_string(), "B=C".to_string()));
|
||||||
|
|
||||||
|
let err = parse_secret_pairs(&["NOPE".to_string()]).unwrap_err();
|
||||||
|
assert_eq!(err.to_string(), "Invalid secret format: NOPE");
|
||||||
|
|
||||||
|
let err = parse_secret_pairs(&["=value".to_string()]).unwrap_err();
|
||||||
|
assert_eq!(err.to_string(), "Secret key cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_reads_valid_file_and_reports_errors() {
|
||||||
|
let temp_root = std::env::temp_dir().join(format!("harbor_config_{}", Uuid::new_v4()));
|
||||||
|
std::fs::create_dir_all(&temp_root).expect("create temp dir");
|
||||||
|
|
||||||
|
let config_path = temp_root.join(".harbor.toml");
|
||||||
|
std::fs::write(
|
||||||
|
&config_path,
|
||||||
|
"version = \"1\"\nname = \"proj\"\nconfig = \"dev\"\n",
|
||||||
|
)
|
||||||
|
.expect("write config");
|
||||||
|
|
||||||
|
let config = Config::from_repo_root(&temp_root).expect("read config");
|
||||||
|
assert_eq!(config.name, "proj");
|
||||||
|
assert_eq!(config.version, "1");
|
||||||
|
assert_eq!(config.default_env, Environment::Dev);
|
||||||
|
|
||||||
|
let invalid_path = temp_root.join("bad.toml");
|
||||||
|
std::fs::write(&invalid_path, "not = [valid").expect("write invalid");
|
||||||
|
let err = Config::read_main_file(&invalid_path).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("TOML error"));
|
||||||
|
|
||||||
|
let missing_path = temp_root.join("missing.toml");
|
||||||
|
let err = Config::read_main_file(&missing_path).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("IO error"));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_and_secret_lifecycle() {
|
||||||
|
let _guard = test_lock().lock().expect("lock tests");
|
||||||
|
let db_path = setup_test_db();
|
||||||
|
let store = SqliteStore::new(String::new());
|
||||||
|
|
||||||
|
assert!(!store.project_exists("alpha").expect("project exists"));
|
||||||
|
|
||||||
|
store.create_project("alpha").expect("create project");
|
||||||
|
assert!(store.project_exists("alpha").expect("project exists"));
|
||||||
|
|
||||||
|
let project_id = store.get_project_id("alpha").expect("get project id");
|
||||||
|
let projects = store.get_projects().expect("get projects");
|
||||||
|
assert_eq!(projects.len(), 1);
|
||||||
|
assert_eq!(projects[0].name, "alpha");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!store
|
||||||
|
.secret_exists(&project_id, "API_KEY", Environment::Dev)
|
||||||
|
.expect("secret exists")
|
||||||
|
);
|
||||||
|
|
||||||
|
let nonce = crypto::helper::gen_nonce();
|
||||||
|
store
|
||||||
|
.set_secret(
|
||||||
|
&project_id,
|
||||||
|
"API_KEY",
|
||||||
|
b"first".to_vec(),
|
||||||
|
Environment::Dev,
|
||||||
|
nonce,
|
||||||
|
)
|
||||||
|
.expect("set secret");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
store
|
||||||
|
.secret_exists(&project_id, "API_KEY", Environment::Dev)
|
||||||
|
.expect("secret exists")
|
||||||
|
);
|
||||||
|
|
||||||
|
let secrets = store
|
||||||
|
.get_project_secrets(&project_id, Environment::Dev)
|
||||||
|
.expect("get secrets");
|
||||||
|
assert_eq!(secrets.len(), 1);
|
||||||
|
assert_eq!(secrets[0].0, "API_KEY");
|
||||||
|
assert_eq!(secrets[0].1, b"first".to_vec());
|
||||||
|
|
||||||
|
let nonce = crypto::helper::gen_nonce();
|
||||||
|
store
|
||||||
|
.set_secret(
|
||||||
|
&project_id,
|
||||||
|
"API_KEY",
|
||||||
|
b"second".to_vec(),
|
||||||
|
Environment::Dev,
|
||||||
|
nonce,
|
||||||
|
)
|
||||||
|
.expect("update secret");
|
||||||
|
|
||||||
|
let updated = store
|
||||||
|
.get_project_secrets(&project_id, Environment::Dev)
|
||||||
|
.expect("get secrets");
|
||||||
|
assert_eq!(updated.len(), 1);
|
||||||
|
assert_eq!(updated[0].1, b"second".to_vec());
|
||||||
|
|
||||||
|
store
|
||||||
|
.delete_secret(&project_id, "API_KEY", Environment::Dev)
|
||||||
|
.expect("delete secret");
|
||||||
|
assert!(
|
||||||
|
!store
|
||||||
|
.secret_exists(&project_id, "API_KEY", Environment::Dev)
|
||||||
|
.expect("secret exists")
|
||||||
|
);
|
||||||
|
|
||||||
|
store.delete_project("alpha").expect("delete project");
|
||||||
|
assert!(!store.project_exists("alpha").expect("project exists"));
|
||||||
|
|
||||||
|
cleanup_test_db(db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn errors_for_missing_project_or_secret() {
|
||||||
|
let _guard = test_lock().lock().expect("lock tests");
|
||||||
|
let db_path = setup_test_db();
|
||||||
|
let store = SqliteStore::new(String::new());
|
||||||
|
|
||||||
|
let err = store.delete_project("missing").unwrap_err();
|
||||||
|
assert_eq!(err.to_string(), "Project does not exist");
|
||||||
|
|
||||||
|
let err = store.get_project_id("missing").unwrap_err();
|
||||||
|
assert_eq!(err.to_string(), "Project not found");
|
||||||
|
|
||||||
|
let missing_project_id = Uuid::new_v4().to_string();
|
||||||
|
let err = store
|
||||||
|
.delete_secret(&missing_project_id, "missing", Environment::Dev)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.to_string(), "Secret does not exist");
|
||||||
|
|
||||||
|
cleanup_test_db(db_path);
|
||||||
|
}
|
||||||
@@ -63,4 +63,39 @@ mod tests {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helper_generates_expected_sizes() {
|
||||||
|
let key = helper::gen_key();
|
||||||
|
let nonce = helper::gen_nonce();
|
||||||
|
|
||||||
|
assert_eq!(key.as_slice().len(), 32);
|
||||||
|
assert_eq!(nonce.as_slice().len(), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_from_validates_length() {
|
||||||
|
let key = helper::key_from(vec![7u8; 32]).expect("expected valid key");
|
||||||
|
assert_eq!(key.as_slice(), vec![7u8; 32]);
|
||||||
|
|
||||||
|
let err = helper::key_from(vec![1u8; 31]).unwrap_err();
|
||||||
|
assert_eq!(err.to_string(), "key must be exactly 32 bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decrypt_fails_with_wrong_key_or_tampered_ciphertext() {
|
||||||
|
let key = helper::gen_key();
|
||||||
|
let other_key = helper::gen_key();
|
||||||
|
let plaintext = b"secret".to_vec();
|
||||||
|
|
||||||
|
let (nonce, mut ciphertext) = encrypt(&key, plaintext.clone()).expect("encrypt");
|
||||||
|
|
||||||
|
assert!(decrypt(&other_key, ciphertext.clone(), &nonce).is_err());
|
||||||
|
|
||||||
|
if let Some(byte) = ciphertext.get_mut(0) {
|
||||||
|
*byte ^= 0b1010_1010;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(decrypt(&key, ciphertext, &nonce).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.8.9", features = ["tokio", "ws"] }
|
||||||
|
tokio = { version = "1.52.3", features = ["full"] }
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use axum::{Router, routing::get};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
fn data_dir() -> PathBuf {
|
||||||
|
std::env::var("HARBOR_DATA_DIR")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|_| PathBuf::from("/var/lib/harbor"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let app = Router::new().route("/health", get(health_check));
|
||||||
|
|
||||||
|
let address = {
|
||||||
|
let port = std::env::var("PORT")
|
||||||
|
.unwrap_or_else(|_| "8080".to_string())
|
||||||
|
.parse::<u16>()
|
||||||
|
.expect("PORT must be a valid u16");
|
||||||
|
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||||
|
format!("{}:{}", host, port)
|
||||||
|
};
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(&address)
|
||||||
|
.await
|
||||||
|
.expect("Failed to bind to address");
|
||||||
|
|
||||||
|
println!("Server running on http://{}", address);
|
||||||
|
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check() -> &'static str {
|
||||||
|
"OK"
|
||||||
|
}
|
||||||
+35
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
@@ -12,6 +13,8 @@
|
|||||||
sqlite
|
sqlite
|
||||||
];
|
];
|
||||||
|
|
||||||
|
env.RUST_BACKTRACE = "1";
|
||||||
|
|
||||||
languages.rust = {
|
languages.rust = {
|
||||||
enable = true;
|
enable = true;
|
||||||
lsp.enable = true;
|
lsp.enable = true;
|
||||||
@@ -24,12 +27,44 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
processes.server =
|
||||||
|
let
|
||||||
|
port = config.processes.server.ports.server.value;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
ports.server.allocate = 8080;
|
||||||
|
exec = "PORT=${toString port} HOST=127.0.0.1 cargo watch -s' cargo run -p server'";
|
||||||
|
ready = {
|
||||||
|
http.get = {
|
||||||
|
port = port;
|
||||||
|
path = "/health";
|
||||||
|
host = "127.0.0.1";
|
||||||
|
scheme = "http";
|
||||||
|
};
|
||||||
|
initial_delay = 5;
|
||||||
|
period = 5;
|
||||||
|
probe_timeout = 3;
|
||||||
|
success_threshold = 1;
|
||||||
|
failure_threshold = 3;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
dotenv.enable = true;
|
dotenv.enable = true;
|
||||||
|
|
||||||
enterTest = ''
|
enterTest = ''
|
||||||
cargo test
|
cargo test
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
profiles.website.module = {
|
||||||
|
languages.javascript = {
|
||||||
|
enable = true;
|
||||||
|
bun = {
|
||||||
|
enable = true;
|
||||||
|
install.enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
git-hooks.hooks = {
|
git-hooks.hooks = {
|
||||||
shellcheck.enable = true;
|
shellcheck.enable = true;
|
||||||
prettier.enable = true;
|
prettier.enable = true;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
../../CLAUDE.md
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
||||||
|
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
Default to using Bun instead of Node.js.
|
||||||
|
|
||||||
|
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||||
|
- Use `bun test` instead of `jest` or `vitest`
|
||||||
|
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||||
|
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||||
|
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||||
|
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
||||||
|
- Bun automatically loads .env, so don't use dotenv.
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
|
||||||
|
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||||
|
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||||
|
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||||
|
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||||
|
- `WebSocket` is built-in. Don't use `ws`.
|
||||||
|
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||||
|
- Bun.$`ls` instead of execa.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Use `bun test` to run tests.
|
||||||
|
|
||||||
|
```ts#index.test.ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
|
test("hello world", () => {
|
||||||
|
expect(1).toBe(1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||||
|
|
||||||
|
Server:
|
||||||
|
|
||||||
|
```ts#index.ts
|
||||||
|
import index from "./index.html"
|
||||||
|
|
||||||
|
Bun.serve({
|
||||||
|
routes: {
|
||||||
|
"/": index,
|
||||||
|
"/api/users/:id": {
|
||||||
|
GET: (req) => {
|
||||||
|
return new Response(JSON.stringify({ id: req.params.id }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// optional websocket support
|
||||||
|
websocket: {
|
||||||
|
open: (ws) => {
|
||||||
|
ws.send("Hello, world!");
|
||||||
|
},
|
||||||
|
message: (ws, message) => {
|
||||||
|
ws.send(message);
|
||||||
|
},
|
||||||
|
close: (ws) => {
|
||||||
|
// handle close
|
||||||
|
}
|
||||||
|
},
|
||||||
|
development: {
|
||||||
|
hmr: true,
|
||||||
|
console: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||||
|
|
||||||
|
```html#index.html
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<script type="module" src="./frontend.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
With the following `frontend.tsx`:
|
||||||
|
|
||||||
|
```tsx#frontend.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
// import .css files directly and it works
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const root = createRoot(document.body);
|
||||||
|
|
||||||
|
export default function Frontend() {
|
||||||
|
return <h1>Hello, world!</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.render(<Frontend />);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run index.ts
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun --hot ./index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# bun-react-tailwind-shadcn-template
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
To run for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun start
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.3.13. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import tailwind from "bun-plugin-tailwind";
|
||||||
|
import { rm } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const outdir = path.join(process.cwd(), "dist");
|
||||||
|
await rm(outdir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
const entrypoints = [...new Bun.Glob("src/**/*.html").scanSync()];
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints,
|
||||||
|
outdir,
|
||||||
|
plugins: [tailwind],
|
||||||
|
minify: true,
|
||||||
|
target: "browser",
|
||||||
|
sourcemap: "linked",
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const output of result.outputs) {
|
||||||
|
console.log(` ${path.relative(process.cwd(), output.path)} ${(output.size / 1024).toFixed(1)} KB`);
|
||||||
|
}
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
// Generated by `bun init`
|
||||||
|
|
||||||
|
declare module "*.svg" {
|
||||||
|
/**
|
||||||
|
* A path to the SVG file
|
||||||
|
*/
|
||||||
|
const path: `${string}.svg`;
|
||||||
|
export = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.css" {}
|
||||||
|
|
||||||
|
declare module "*.module.css" {
|
||||||
|
/**
|
||||||
|
* A record of class names to their corresponding CSS module classes
|
||||||
|
*/
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export = classes;
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "bun-react-template",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"bun-plugin-tailwind": "^0.1.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^1",
|
||||||
|
"react": "^19",
|
||||||
|
"react-dom": "^19",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
|
||||||
|
|
||||||
|
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
|
||||||
|
|
||||||
|
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="],
|
||||||
|
|
||||||
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
||||||
|
|
||||||
|
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Omj20SuiHBOUjUBIyqtkNjSUIjOtEOJwmbix/ZyFH4BaQ6OZTaaRWIR4TjHVz0yadHgli6lLTiAh1uarnvD49A=="],
|
||||||
|
|
||||||
|
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-FFj3QdU/OhlDyZOJ8CWfN5eWLpRlT4qjZg7lMQi7jA6GuoY5ajlO1zWLP/MuHYRSbXQUvV52RejNi8DVnAp13w=="],
|
||||||
|
|
||||||
|
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-OSfsTZstc898HHElhU4NccaBGOSSDn5VfahiVTnidZ9B/+wb7WTyfZJaBeJcfjwJ9H2W9uTh2TGtl3UfcXgV9g=="],
|
||||||
|
|
||||||
|
"@oven/bun-freebsd-aarch64": ["@oven/bun-freebsd-aarch64@1.3.14", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-LIKrXaFxAHybVO5Pf+9XP2FHUj/5APvXTUKk9dqHm5iFz4oH+W24cmhjkJirNujh9hKeTyrpWSe3no9JZKowIw=="],
|
||||||
|
|
||||||
|
"@oven/bun-freebsd-x64": ["@oven/bun-freebsd-x64@1.3.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uwD+fGUH1ADpIF3B1U2jWzzb20QwRLZfj5QZ28GUCGrAJ/nTmWrD6YYGsblCY1wuhldRez3lU40AyuvSCyLYmw=="],
|
||||||
|
|
||||||
|
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-X5SsPZHs+iYO8R/efIcRtc7gT2Q2DgPfliCxEkx4cXBumwkw0c/EsHMNwH3EgGpCDaZ7IYVPhpCG/xBOQHEwZw=="],
|
||||||
|
|
||||||
|
"@oven/bun-linux-aarch64-android": ["@oven/bun-linux-aarch64-android@1.3.14", "", { "os": "android", "cpu": "arm64" }, "sha512-y4kq5b85lsrmFb9Xvi4w9mA5IEFJkLMrSmYn06q24KjL9rUWDWO3VFZEtteZxUN5+ec3Zm5S8OnJw1umaCbVjA=="],
|
||||||
|
|
||||||
|
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmqOA92Cd1NL/1XBd4bFkJLxQ86K0RW7ohxS2qzzAvuitO4JiIxjjTeCspoU44zCozH72HpfZfUE2On31OjnWA=="],
|
||||||
|
|
||||||
|
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-7OVTAKvwfPmSbIV1HpdOoVVx5VRc427GuPPne93N6vk4eQBPId9nXmZDh9/zGaKPdbVjVtQSZafWQoUjx38Utw=="],
|
||||||
|
|
||||||
|
"@oven/bun-linux-x64-android": ["@oven/bun-linux-x64-android@1.3.14", "", { "os": "android", "cpu": "x64" }, "sha512-qe9e1d+3VAEU7nAA2ol9Jvmy/o99PVMSgZhHn7Q/9O3YcDrfEqyQ8zm4zoe5qTEo8HZH0dN03Le0Ys2eQPs7eg=="],
|
||||||
|
|
||||||
|
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-q/8EdOC0yUE8FPeoOVq8/Pw5I9/tJaYmUfO/uDUAREx8IUnOJH1RJ5A3BjFqre8pvJoiZA9AovPJq5FnNNjSxA=="],
|
||||||
|
|
||||||
|
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-GBCB/k/sIqcr06eTNgg7g46qiUv35Jasx4XiccJ/n7RGqrE4RWUD/XJBbWFprVPjvqd59+QtSnS99XGqvftHfg=="],
|
||||||
|
|
||||||
|
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-n6iE71G4lQE4XkrZhQQcL5YUlxDbnq6nqV7zeQi33PMsLT/0kYE+RvHOtBWZ3w0wMdXZfINmp63hIb9ijUBGtw=="],
|
||||||
|
|
||||||
|
"@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.3.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-T7s3x/BsVKQObGU6QDkZeI6wKynzqGbBH1yI77jrrj5siElclxr3DQrDIk8CV4G5/SJq2HHq4kpLyYY2DKCSmA=="],
|
||||||
|
|
||||||
|
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-mUFWL3BoYkNpjd8e9PqROiFF/1Xeotq20mABJsiQH62jM1g5zqWh4khw1RZ6bX8Q8fWvlPaxG1PjofkmjUi3vg=="],
|
||||||
|
|
||||||
|
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-uIjLUC1S9DWgICzuoMba7vurBJnBruE4S5CxnvmZkdqWVXRzx1Rgu636HoH+k0qeaQCFh3jeG3JQ1y6fRHv0sw=="],
|
||||||
|
|
||||||
|
"@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="],
|
||||||
|
|
||||||
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.3", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.11", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.7", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-select": ["@radix-ui/react-select@2.3.3", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.7", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-L5RQTXz6Anxsf9CCv+pTgiAsUpyVj7rJxsGtmhFaEOJ++cVfXucv4qWfsIO0AIB4NAhi3yovWGVMKKS1Xf1Wrg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw=="],
|
||||||
|
|
||||||
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||||
|
|
||||||
|
"bun": ["bun@1.3.14", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.14", "@oven/bun-darwin-x64": "1.3.14", "@oven/bun-darwin-x64-baseline": "1.3.14", "@oven/bun-freebsd-aarch64": "1.3.14", "@oven/bun-freebsd-x64": "1.3.14", "@oven/bun-linux-aarch64": "1.3.14", "@oven/bun-linux-aarch64-android": "1.3.14", "@oven/bun-linux-aarch64-musl": "1.3.14", "@oven/bun-linux-x64": "1.3.14", "@oven/bun-linux-x64-android": "1.3.14", "@oven/bun-linux-x64-baseline": "1.3.14", "@oven/bun-linux-x64-musl": "1.3.14", "@oven/bun-linux-x64-musl-baseline": "1.3.14", "@oven/bun-windows-aarch64": "1.3.14", "@oven/bun-windows-x64": "1.3.14", "@oven/bun-windows-x64-baseline": "1.3.14" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg=="],
|
||||||
|
|
||||||
|
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
|
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
|
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@1.24.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||||
|
|
||||||
|
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||||
|
|
||||||
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
|
|
||||||
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
|
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||||
|
|
||||||
|
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[serve.static]
|
||||||
|
plugins = ["bun-plugin-tailwind"]
|
||||||
|
env = "BUN_PUBLIC_*"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "styles/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "bun-react-template",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun --hot src/index.ts",
|
||||||
|
"start": "NODE_ENV=production bun src/index.ts",
|
||||||
|
"build": "bun run build.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"bun-plugin-tailwind": "^0.1.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^1",
|
||||||
|
"react": "^19",
|
||||||
|
"react-dom": "^19",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tw-animate-css": "^1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"tailwindcss": "^4.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowRight,
|
||||||
|
Cloud,
|
||||||
|
Key,
|
||||||
|
Layers,
|
||||||
|
Lock,
|
||||||
|
Package,
|
||||||
|
Server,
|
||||||
|
Settings,
|
||||||
|
Shield,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
useEffect(() => {
|
||||||
|
const elements = Array.from(document.querySelectorAll<HTMLElement>(".reveal"));
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add("is-visible");
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.forEach(element => observer.observe(element));
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen text-foreground">
|
||||||
|
<div className="relative">
|
||||||
|
<header className="mx-auto flex max-w-6xl items-center justify-between px-6 pt-8" data-reveal>
|
||||||
|
<div className="reveal flex items-center gap-3" data-delay="1">
|
||||||
|
<div className="grid h-11 w-11 place-items-center rounded-2xl bg-primary text-primary-foreground shadow-[0_12px_30px_-16px_rgba(16,24,40,0.6)]">
|
||||||
|
<Layers className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-display text-lg tracking-tight">Harbor</p>
|
||||||
|
<p className="text-[0.65rem] uppercase tracking-[0.32em] text-muted-foreground">Local-first secrets</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav className="hidden items-center gap-6 text-sm font-medium text-muted-foreground md:flex reveal" data-delay="2">
|
||||||
|
<a href="#local" className="transition hover:text-foreground">
|
||||||
|
Local
|
||||||
|
</a>
|
||||||
|
<a href="#server" className="transition hover:text-foreground">
|
||||||
|
Server
|
||||||
|
</a>
|
||||||
|
<a href="#install" className="transition hover:text-foreground">
|
||||||
|
Nix Flake
|
||||||
|
</a>
|
||||||
|
<a href="#capabilities" className="transition hover:text-foreground">
|
||||||
|
Capabilities
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<a
|
||||||
|
href="/install"
|
||||||
|
className="hidden rounded-full border border-foreground/15 bg-card/80 px-4 py-2 text-xs font-semibold uppercase tracking-[0.3em] text-foreground/80 shadow-sm md:inline-flex reveal"
|
||||||
|
data-delay="3"
|
||||||
|
>
|
||||||
|
Install
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="mx-auto flex max-w-6xl flex-col gap-20 px-6 pb-24 pt-12">
|
||||||
|
<section className="grid items-center gap-10 lg:grid-cols-[1.1fr_0.9fr]">
|
||||||
|
<div className="space-y-6" data-reveal>
|
||||||
|
<div className="reveal float-slow inline-flex items-center gap-2 rounded-full border border-foreground/10 bg-card/80 px-4 py-2 text-xs font-semibold uppercase tracking-[0.3em] text-foreground/70">
|
||||||
|
Local + server in sync
|
||||||
|
</div>
|
||||||
|
<h1 className="font-display reveal text-4xl leading-tight tracking-tight sm:text-5xl lg:text-6xl" data-delay="1">
|
||||||
|
Harbor keeps local config and team secrets in lockstep.
|
||||||
|
</h1>
|
||||||
|
<p className="reveal text-base text-muted-foreground sm:text-lg" data-delay="2">
|
||||||
|
Run the CLI locally to read configuration straight from your code. Sync only the projects you want, and
|
||||||
|
let the server manage secrets, configs, and shared updates for the whole team.
|
||||||
|
</p>
|
||||||
|
<div className="reveal flex flex-wrap gap-3" data-delay="3">
|
||||||
|
<a
|
||||||
|
href="/install"
|
||||||
|
className="rounded-full bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground shadow-[0_12px_30px_-16px_rgba(16,24,40,0.6)] transition hover:translate-y-[-1px]"
|
||||||
|
>
|
||||||
|
Install
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#capabilities"
|
||||||
|
className="rounded-full border border-foreground/15 bg-card/80 px-5 py-2.5 text-sm font-semibold text-foreground/80 shadow-sm transition hover:text-foreground"
|
||||||
|
>
|
||||||
|
View capabilities
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="reveal rounded-3xl border border-foreground/10 bg-card/80 p-6 shadow-[0_40px_120px_-80px_rgba(16,24,40,0.6)]" data-reveal>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-muted-foreground">Sync map</p>
|
||||||
|
<span className="rounded-full bg-foreground/5 px-3 py-1 text-xs text-foreground/70">Team-wide</span>
|
||||||
|
</div>
|
||||||
|
<ol className="mt-6 space-y-5 text-sm">
|
||||||
|
<li className="flex gap-3">
|
||||||
|
<Terminal className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-foreground">Local reads the code</p>
|
||||||
|
<p className="text-muted-foreground">The CLI inspects configuration directly in your repo.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex gap-3">
|
||||||
|
<Package className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-foreground">Selective project sync</p>
|
||||||
|
<p className="text-muted-foreground">Only the projects you choose are sent to the server.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex gap-3">
|
||||||
|
<Server className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-foreground">Server control plane</p>
|
||||||
|
<p className="text-muted-foreground">Manage secrets, configs, and shared updates.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex gap-3">
|
||||||
|
<Shield className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-foreground">Everyone stays aligned</p>
|
||||||
|
<p className="text-muted-foreground">Updates remain in sync across your entire team.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="local" className="scroll-mt-24" data-reveal>
|
||||||
|
<div className="reveal grid gap-8 rounded-3xl border border-foreground/10 bg-card/80 p-8 shadow-sm lg:grid-cols-[1.1fr_0.9fr]">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 text-xs uppercase tracking-[0.3em] text-muted-foreground">
|
||||||
|
<Terminal className="h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
Local side
|
||||||
|
</div>
|
||||||
|
<h2 className="font-display mt-3 text-3xl">Local reads the code first.</h2>
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground">
|
||||||
|
Harbor runs next to your repo. Locally, you can just read the code, and the CLI does not require a
|
||||||
|
server to operate.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 space-y-4 text-sm">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Shield className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">No server required</p>
|
||||||
|
<p className="text-muted-foreground">Everything runs locally until you choose to sync.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Settings className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Understands your config</p>
|
||||||
|
<p className="text-muted-foreground">Reads configuration straight from your codebase.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Package className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Selective project sync</p>
|
||||||
|
<p className="text-muted-foreground">Choose exactly which projects get shared.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="#server"
|
||||||
|
className="mt-6 inline-flex items-center gap-2 rounded-full border border-foreground/15 bg-background px-4 py-2 text-xs font-semibold uppercase tracking-[0.3em] text-foreground/80"
|
||||||
|
>
|
||||||
|
Switch to server
|
||||||
|
<ArrowDown className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-between gap-6 rounded-2xl border border-foreground/10 bg-background/60 p-5">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-muted-foreground">Local workflow</p>
|
||||||
|
<h3 className="font-display mt-3 text-xl">CLI stays close to the repo.</h3>
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground">
|
||||||
|
Read configuration where it lives, keep secrets local, and sync only when needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-foreground/5 p-4 text-xs text-foreground/70">
|
||||||
|
<p className="font-semibold text-foreground">Local-first cadence</p>
|
||||||
|
<p className="mt-2">Inspect config -> Select projects -> Sync to server</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="server" className="scroll-mt-24" data-reveal>
|
||||||
|
<div className="reveal grid gap-8 rounded-3xl border border-foreground/10 bg-card/80 p-8 shadow-sm lg:grid-cols-[0.9fr_1.1fr]">
|
||||||
|
<div className="order-2 lg:order-1 flex flex-col justify-between gap-6 rounded-2xl border border-foreground/10 bg-background/60 p-5">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-muted-foreground">Server overview</p>
|
||||||
|
<h3 className="font-display mt-3 text-xl">Teamwide management hub.</h3>
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground">
|
||||||
|
Use the local CLI to sync specific projects, then manage them on the server with full control.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-foreground/5 p-4 text-xs text-foreground/70">
|
||||||
|
<p className="font-semibold text-foreground">Server control</p>
|
||||||
|
<p className="mt-2">Edit secrets -> Create configs -> Sync to everyone</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="order-1 lg:order-2">
|
||||||
|
<div className="flex items-center gap-3 text-xs uppercase tracking-[0.3em] text-muted-foreground">
|
||||||
|
<Server className="h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
Server side
|
||||||
|
</div>
|
||||||
|
<h2 className="font-display mt-3 text-3xl">Manage secrets for the whole team.</h2>
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground">
|
||||||
|
Use the local CLI to sync selected projects. The server then manages those projects, including secrets
|
||||||
|
and configuration.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 space-y-4 text-sm">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Key className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Edit secrets and configs</p>
|
||||||
|
<p className="text-muted-foreground">Update values and create new configuration sets.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Lock className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Generate new secrets</p>
|
||||||
|
<p className="text-muted-foreground">Create fresh values whenever you need them.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Cloud className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Stay in sync</p>
|
||||||
|
<p className="text-muted-foreground">Every update stays aligned across your entire team.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="install" className="scroll-mt-24" data-reveal>
|
||||||
|
<div className="reveal flex flex-wrap items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-muted-foreground">Install</p>
|
||||||
|
<h2 className="font-display mt-3 text-3xl">Add Harbor as a flake input</h2>
|
||||||
|
</div>
|
||||||
|
<p className="max-w-md text-sm text-muted-foreground">
|
||||||
|
Keep the flake input on your repo, then use the install page for Cargo and manual builds.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="reveal mt-8 rounded-2xl border border-foreground/10 bg-card/80 p-6 shadow-sm" data-delay="1">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Package className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||||
|
<h3 className="font-display text-xl">Nix flake input</h3>
|
||||||
|
</div>
|
||||||
|
<pre className="mt-4 rounded-xl bg-foreground/5 p-4 text-xs text-foreground/80">
|
||||||
|
<code>{`inputs = {
|
||||||
|
harbor = {
|
||||||
|
url = "git+https://gitea.maariz.org/pure_sagacity/harbor.git";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};`}</code>
|
||||||
|
</pre>
|
||||||
|
<div className="mt-5 flex flex-wrap items-center justify-between gap-4 text-sm">
|
||||||
|
<span className="text-muted-foreground">Need Cargo or manual?</span>
|
||||||
|
<a href="/install" className="inline-flex items-center gap-2 font-semibold text-primary">
|
||||||
|
View other install methods
|
||||||
|
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="capabilities" className="scroll-mt-24" data-reveal>
|
||||||
|
<div className="reveal flex flex-wrap items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-muted-foreground">Capabilities</p>
|
||||||
|
<h2 className="font-display mt-3 text-3xl">What Harbor can do</h2>
|
||||||
|
</div>
|
||||||
|
<p className="max-w-md text-sm text-muted-foreground">
|
||||||
|
Local-first by design, with server-side controls that keep your team aligned.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: "Read config from code",
|
||||||
|
description: "The CLI parses configuration directly from your repo.",
|
||||||
|
icon: Terminal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Project-level control",
|
||||||
|
description: "Sync only the projects you choose to share.",
|
||||||
|
icon: Package,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Manage projects",
|
||||||
|
description: "Organize synced projects and environments.",
|
||||||
|
icon: Layers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Edit and update",
|
||||||
|
description: "Change secrets from the server and share updates.",
|
||||||
|
icon: Key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Create new configs",
|
||||||
|
description: "Build new configuration sets server-side.",
|
||||||
|
icon: Settings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Always aligned",
|
||||||
|
description: "Generate fresh secrets and keep every teammate in sync.",
|
||||||
|
icon: Shield,
|
||||||
|
},
|
||||||
|
].map((item, index) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.title}
|
||||||
|
className="reveal rounded-2xl border border-foreground/10 bg-card/80 p-5 shadow-sm"
|
||||||
|
data-reveal
|
||||||
|
data-delay={index % 3 === 0 ? "1" : index % 3 === 1 ? "2" : "3"}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Icon className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-muted-foreground">Capability</p>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-display mt-3 text-lg">{item.title}</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="reveal rounded-3xl border border-foreground/10 bg-card/80 p-8 shadow-sm" data-reveal>
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-muted-foreground">Teamwide consistency</p>
|
||||||
|
<h2 className="font-display mt-3 text-3xl">Every update stays in sync across your entire team.</h2>
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground">
|
||||||
|
Sync a project once, then manage secrets and configs on the server. Harbor keeps local and server states
|
||||||
|
aligned for everyone using the CLI.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 flex flex-wrap gap-3 text-xs font-semibold uppercase tracking-[0.24em] text-foreground/70">
|
||||||
|
<span className="rounded-full border border-foreground/15 bg-background px-3 py-1">Local-first</span>
|
||||||
|
<span className="rounded-full border border-foreground/15 bg-background px-3 py-1">Selective sync</span>
|
||||||
|
<span className="rounded-full border border-foreground/15 bg-background px-3 py-1">Shared secrets</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-3 px-6 pb-10 text-xs text-muted-foreground">
|
||||||
|
<span>Harbor CLI + Server</span>
|
||||||
|
<span>Local-first secrets and configuration sync.</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { ArrowLeft, ArrowRight, Layers, Package, Terminal, Wrench } from "lucide-react";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
export function InstallApp() {
|
||||||
|
useEffect(() => {
|
||||||
|
const elements = Array.from(document.querySelectorAll<HTMLElement>(".reveal"));
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add("is-visible");
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.forEach(element => observer.observe(element));
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen text-foreground">
|
||||||
|
<div className="relative">
|
||||||
|
<header className="mx-auto flex max-w-6xl items-center justify-between px-6 pt-8" data-reveal>
|
||||||
|
<a href="/" className="reveal flex items-center gap-3" data-delay="1">
|
||||||
|
<div className="grid h-11 w-11 place-items-center rounded-2xl bg-primary text-primary-foreground shadow-[0_12px_30px_-16px_rgba(16,24,40,0.6)]">
|
||||||
|
<Layers className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-display text-lg tracking-tight">Harbor</p>
|
||||||
|
<p className="text-[0.65rem] uppercase tracking-[0.32em] text-muted-foreground">Install</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="hidden items-center gap-2 rounded-full border border-foreground/15 bg-card/80 px-4 py-2 text-xs font-semibold uppercase tracking-[0.3em] text-foreground/80 shadow-sm md:inline-flex reveal"
|
||||||
|
data-delay="2"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
|
Back to home
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="mx-auto flex max-w-6xl flex-col gap-16 px-6 pb-24 pt-12">
|
||||||
|
<section className="grid items-center gap-10 lg:grid-cols-[1.1fr_0.9fr]" data-reveal>
|
||||||
|
<div className="reveal space-y-6">
|
||||||
|
<div className="float-slow inline-flex items-center gap-2 rounded-full border border-foreground/10 bg-card/80 px-4 py-2 text-xs font-semibold uppercase tracking-[0.3em] text-foreground/70">
|
||||||
|
All install methods
|
||||||
|
</div>
|
||||||
|
<h1 className="font-display text-4xl leading-tight tracking-tight sm:text-5xl lg:text-6xl">
|
||||||
|
Install Harbor locally, the way your team prefers.
|
||||||
|
</h1>
|
||||||
|
<p className="text-base text-muted-foreground sm:text-lg">
|
||||||
|
Pick Cargo for a quick install or build manually with the Rust toolchain. The Nix flake input lives on
|
||||||
|
the main landing page.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center gap-2 rounded-full border border-foreground/15 bg-background px-4 py-2 text-xs font-semibold uppercase tracking-[0.3em] text-foreground/80"
|
||||||
|
>
|
||||||
|
View flake input
|
||||||
|
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="reveal rounded-3xl border border-foreground/10 bg-card/80 p-6 shadow-sm" data-delay="2">
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] text-muted-foreground">Install checklist</p>
|
||||||
|
<ul className="mt-5 space-y-4 text-sm">
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<Terminal className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Rust toolchain</p>
|
||||||
|
<p className="text-muted-foreground">Ensure Rust is installed before running commands.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<Package className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Cargo install</p>
|
||||||
|
<p className="text-muted-foreground">Grab the CLI directly from crates.io.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<Wrench className="mt-1 h-4 w-4 text-primary" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Manual build</p>
|
||||||
|
<p className="text-muted-foreground">Build from source with one command.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-2xl border border-foreground/10 bg-card/80 p-6 shadow-sm reveal" data-reveal>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Layers className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||||
|
<h2 className="font-display text-2xl">Nix flake input</h2>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground">Add Harbor as a flake input in your repo.</p>
|
||||||
|
<pre className="mt-5 rounded-xl bg-foreground/5 p-4 text-xs text-foreground/80">
|
||||||
|
<code>{`inputs = {
|
||||||
|
harbor = {
|
||||||
|
url = "git+https://gitea.maariz.org/pure_sagacity/harbor.git";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};`}</code>
|
||||||
|
</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-6 md:grid-cols-2" data-reveal>
|
||||||
|
<div className="reveal rounded-2xl border border-foreground/10 bg-card/80 p-6 shadow-sm" data-delay="1">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Package className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||||
|
<h2 className="font-display text-2xl">Cargo install</h2>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground">Install from crates.io.</p>
|
||||||
|
<pre className="mt-5 rounded-xl bg-foreground/5 p-4 text-xs text-foreground/80">
|
||||||
|
<code>cargo install harbor</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="reveal rounded-2xl border border-foreground/10 bg-card/80 p-6 shadow-sm" data-delay="2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Wrench className="h-5 w-5 text-primary" aria-hidden="true" />
|
||||||
|
<h2 className="font-display text-2xl">Manual build</h2>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground">Build the CLI directly from the repo.</p>
|
||||||
|
<pre className="mt-5 rounded-xl bg-foreground/5 p-4 text-xs text-foreground/80">
|
||||||
|
<code>cargo build -p cli --release</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-3 px-6 pb-10 text-xs text-muted-foreground">
|
||||||
|
<span>Harbor install</span>
|
||||||
|
<span>Choose Cargo or manual builds.</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InstallApp;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div data-slot="card-footer" className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input };
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label };
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "popper",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* This file is the entry point for the React app, it sets up the root
|
||||||
|
* element and renders the App component to the DOM.
|
||||||
|
*
|
||||||
|
* It is included in `src/index.html`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { App } from "./App";
|
||||||
|
|
||||||
|
const elem = document.getElementById("root")!;
|
||||||
|
const app = (
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
// https://bun.com/docs/bundler/hot-reloading#import-meta-hot-data
|
||||||
|
(import.meta.hot.data.root ??= createRoot(elem)).render(app);
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
|
||||||
|
@import "../styles/globals.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-body: "Inter", "Source Sans 3", sans-serif;
|
||||||
|
--font-display: "Inter", "Source Sans 3", sans-serif;
|
||||||
|
--harbor-ink: oklch(0.18 0.03 230);
|
||||||
|
--harbor-sea: oklch(0.46 0.12 215);
|
||||||
|
--harbor-sea-deep: oklch(0.34 0.1 230);
|
||||||
|
--harbor-sand: oklch(0.97 0.02 90);
|
||||||
|
--harbor-coral: oklch(0.75 0.12 45);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply m-0 min-h-screen bg-background text-foreground antialiased;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-display {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px);
|
||||||
|
transition: opacity 0.7s ease, transform 0.7s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal[data-delay="1"] {
|
||||||
|
transition-delay: 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal[data-delay="2"] {
|
||||||
|
transition-delay: 240ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal[data-delay="3"] {
|
||||||
|
transition-delay: 360ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-slow {
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-fast {
|
||||||
|
animation: float 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion) {
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
|
||||||
|
<title>Harbor</title>
|
||||||
|
<script type="module" src="./frontend.tsx" async></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { serve } from "bun";
|
||||||
|
import index from "./index.html";
|
||||||
|
import install from "./install.html";
|
||||||
|
|
||||||
|
const server = serve({
|
||||||
|
routes: {
|
||||||
|
"/*": index,
|
||||||
|
|
||||||
|
"/install": install,
|
||||||
|
"/install/": install,
|
||||||
|
},
|
||||||
|
|
||||||
|
development: process.env.NODE_ENV !== "production" && {
|
||||||
|
// Enable browser hot reloading in development
|
||||||
|
hmr: true,
|
||||||
|
|
||||||
|
// Echo console logs from the browser to the server
|
||||||
|
console: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🚀 Server running at ${server.url}`);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
|
||||||
|
<title>Install Harbor CLI</title>
|
||||||
|
<script type="module" src="./install.tsx" async></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Install page entry point.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { InstallApp } from "./InstallApp";
|
||||||
|
|
||||||
|
const elem = document.getElementById("root")!;
|
||||||
|
const app = (
|
||||||
|
<StrictMode>
|
||||||
|
<InstallApp />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
(import.meta.hot.data.root ??= createRoot(elem)).render(app);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
|
||||||
|
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
|
||||||
|
<g stroke="#61dafb" stroke-width="1" fill="none">
|
||||||
|
<ellipse rx="11" ry="4.2"/>
|
||||||
|
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
|
||||||
|
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 338 B |
@@ -0,0 +1,120 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.75rem;
|
||||||
|
--background: oklch(0.9 0.04 222);
|
||||||
|
--foreground: oklch(0.18 0.03 230);
|
||||||
|
--card: oklch(0.98 0.015 220);
|
||||||
|
--card-foreground: oklch(0.18 0.03 230);
|
||||||
|
--popover: oklch(0.98 0.015 220);
|
||||||
|
--popover-foreground: oklch(0.18 0.03 230);
|
||||||
|
--primary: oklch(0.42 0.12 222);
|
||||||
|
--primary-foreground: oklch(0.98 0.02 220);
|
||||||
|
--secondary: oklch(0.92 0.03 220);
|
||||||
|
--secondary-foreground: oklch(0.26 0.03 230);
|
||||||
|
--muted: oklch(0.94 0.02 220);
|
||||||
|
--muted-foreground: oklch(0.46 0.03 230);
|
||||||
|
--accent: oklch(0.84 0.07 205);
|
||||||
|
--accent-foreground: oklch(0.2 0.03 230);
|
||||||
|
--destructive: oklch(0.58 0.22 25);
|
||||||
|
--border: oklch(0.84 0.03 220);
|
||||||
|
--input: oklch(0.9 0.02 220);
|
||||||
|
--ring: oklch(0.58 0.1 220);
|
||||||
|
--chart-1: oklch(0.6 0.14 220);
|
||||||
|
--chart-2: oklch(0.66 0.13 160);
|
||||||
|
--chart-3: oklch(0.55 0.1 45);
|
||||||
|
--chart-4: oklch(0.72 0.14 80);
|
||||||
|
--chart-5: oklch(0.62 0.16 20);
|
||||||
|
--sidebar: oklch(0.9 0.04 222);
|
||||||
|
--sidebar-foreground: oklch(0.18 0.03 230);
|
||||||
|
--sidebar-primary: oklch(0.42 0.12 222);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0.02 220);
|
||||||
|
--sidebar-accent: oklch(0.92 0.03 220);
|
||||||
|
--sidebar-accent-foreground: oklch(0.26 0.03 230);
|
||||||
|
--sidebar-border: oklch(0.84 0.03 220);
|
||||||
|
--sidebar-ring: oklch(0.58 0.1 220);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.2 0.03 230);
|
||||||
|
--foreground: oklch(0.95 0.015 90);
|
||||||
|
--card: oklch(0.23 0.03 230);
|
||||||
|
--card-foreground: oklch(0.95 0.015 90);
|
||||||
|
--popover: oklch(0.23 0.03 230);
|
||||||
|
--popover-foreground: oklch(0.95 0.015 90);
|
||||||
|
--primary: oklch(0.82 0.08 80);
|
||||||
|
--primary-foreground: oklch(0.2 0.03 230);
|
||||||
|
--secondary: oklch(0.3 0.03 230);
|
||||||
|
--secondary-foreground: oklch(0.95 0.015 90);
|
||||||
|
--muted: oklch(0.3 0.03 230);
|
||||||
|
--muted-foreground: oklch(0.74 0.02 90);
|
||||||
|
--accent: oklch(0.32 0.04 230);
|
||||||
|
--accent-foreground: oklch(0.95 0.015 90);
|
||||||
|
--destructive: oklch(0.62 0.2 25);
|
||||||
|
--border: oklch(0.95 0.015 90 / 12%);
|
||||||
|
--input: oklch(0.95 0.015 90 / 18%);
|
||||||
|
--ring: oklch(0.62 0.08 215);
|
||||||
|
--chart-1: oklch(0.62 0.14 220);
|
||||||
|
--chart-2: oklch(0.68 0.13 160);
|
||||||
|
--chart-3: oklch(0.72 0.12 70);
|
||||||
|
--chart-4: oklch(0.66 0.12 40);
|
||||||
|
--chart-5: oklch(0.64 0.16 20);
|
||||||
|
--sidebar: oklch(0.23 0.03 230);
|
||||||
|
--sidebar-foreground: oklch(0.95 0.015 90);
|
||||||
|
--sidebar-primary: oklch(0.72 0.1 200);
|
||||||
|
--sidebar-primary-foreground: oklch(0.2 0.03 230);
|
||||||
|
--sidebar-accent: oklch(0.3 0.03 230);
|
||||||
|
--sidebar-accent-foreground: oklch(0.95 0.015 90);
|
||||||
|
--sidebar-border: oklch(0.95 0.015 90 / 12%);
|
||||||
|
--sidebar-ring: oklch(0.62 0.08 215);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
"types": ["bun", "react"],
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"exclude": ["dist", "node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user