From c20f8d6d5f792aa55767d5625dced660d8bb2c74 Mon Sep 17 00:00:00 2001 From: hdbg Date: Wed, 3 Dec 2025 17:28:33 +0100 Subject: [PATCH] misc: initial commit --- .gitignore | 1 + CLAUDE.md | 109 +++ Cargo.lock | 2075 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 19 + pgx.toml | 3 + src/banner.txt | 8 + src/cli.rs | 64 ++ src/config.rs | 174 ++++ src/controller.rs | 632 ++++++++++++++ src/main.rs | 39 + src/state.rs | 203 +++++ 11 files changed, 3327 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 pgx.toml create mode 100644 src/banner.txt create mode 100644 src/cli.rs create mode 100644 src/config.rs create mode 100644 src/controller.rs create mode 100644 src/main.rs create mode 100644 src/state.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..40eea88 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,109 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`pgx` is a CLI tool for managing project-scoped PostgreSQL instances running in Docker containers. Each project gets its own isolated Postgres instance, managed through a `pgx.toml` configuration file. + +## Core Architecture + +### Project-Oriented Design + +- Each project has a `pgx.toml` file at its root that defines the Postgres configuration +- The project name is derived from the directory containing `pgx.toml` +- Each project gets its own Docker container +- State is tracked separately per instance to detect configuration drift + +### Configuration Management + +The `pgx.toml` file stores: +- `postgres_version`: PostgreSQL version to use +- `database_name`: Name of the database +- `user_name`: Database user +- `password`: Database password +- `port`: Host port to bind (auto-selected from available ports) + +Values are auto-populated during `pgx init` with sensible defaults or random values where appropriate. + +### State Tracking + +The tool maintains separate state for each instance to detect configuration drift, such as: +- Container's actual Postgres version vs. config file version +- Running container state vs. expected state +- Port conflicts or changes + +## Key Dependencies + +- **clap** (with derive feature): CLI argument parsing and command structure +- **toml**: Parsing and serializing `pgx.toml` configuration files +- **bollard**: Docker daemon interaction for container lifecycle management +- **tokio** (with full feature set): Async runtime for Docker operations +- **tracing** + **tracing-subscriber**: Structured logging throughout the application +- **serde**: Serialization/deserialization for config and state +- **miette** (with fancy feature): Enhanced error reporting +- **prodash** (to be added): Terminal progress bars for long-running operations + +## Development Commands + +### Building +```bash +cargo build +cargo build --release +``` + +### Running +```bash +cargo run -- +# Example: cargo run -- init +``` + +### Testing +```bash +cargo test +cargo test # Run specific test +``` + +### Linting +```bash +cargo clippy +cargo clippy -- -W clippy::all +``` + +### Formatting +```bash +cargo fmt +cargo fmt -- --check # Check without modifying +``` + +## Command Structure + +The CLI follows this pattern: +``` +pgx [options] +``` + +Key commands to implement: +- `pgx init`: Create pgx.toml with auto-populated configuration +- `pgx start`: Start the Postgres container for current project +- `pgx stop`: Stop the running container +- `pgx status`: Show instance status and detect drift +- `pgx destroy`: Remove container and clean up + +## Implementation Notes + +### Port Selection + +Port assignment should scan for available ports on the system rather than using fixed defaults to avoid conflicts. + +### Docker Container Naming + +Container names should be deterministic based on project name to allow easy identification and prevent duplicates. + +### Error Handling + +Use `miette` for user-facing errors with context. Docker operations and file I/O are the primary error sources. + +### Async Operations + +Docker operations via `bollard` are async. Use `tokio` runtime with `#[tokio::main]` on the main function. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..756a5eb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2075 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bollard" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" +dependencies = [ + "base64", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "http", + "http-body-util", + "hyper", + "hyper-named-pipe", + "hyper-util", + "hyperlocal", + "log", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.49.1-rc.28.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623" +dependencies = [ + "serde", + "serde_json", + "serde_repr", + "serde_with", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "console" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +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]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +dependencies = [ + "console", + "portable-atomic", + "unicode-segmentation", + "unicode-width 0.2.2", + "unit-prefix", + "web-time", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pgx" +version = "0.1.0" +dependencies = [ + "bollard", + "clap", + "futures", + "indicatif", + "miette", + "rand", + "serde", + "serde_json", + "serde_with", + "tokio", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "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 = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.1", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.12.1", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..66e70ec --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pgx" +version = "0.1.0" +edition = "2024" + +[dependencies] +bollard = "0.19.4" +clap = { version = "4.5.53", features = ["derive"] } +futures = "0.3.31" +indicatif = { version = "0.18.3", features = ["improved_unicode"] } +miette = { version = "7.6.0", features = ["fancy"] } +rand = "0.9.2" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +serde_with = "3.16.1" +tokio = { version = "1.48.0", features = ["full"] } +toml = "0.9.8" +tracing = "0.1.43" +tracing-subscriber = "0.3.22" diff --git a/pgx.toml b/pgx.toml new file mode 100644 index 0000000..0cf2a56 --- /dev/null +++ b/pgx.toml @@ -0,0 +1,3 @@ +version = "18.1" +password = "odqAsS49KNyaXjrw" +port = 5432 diff --git a/src/banner.txt b/src/banner.txt new file mode 100644 index 0000000..1e73dcc --- /dev/null +++ b/src/banner.txt @@ -0,0 +1,8 @@ + + + _ __ __ ___ __ + | '_ \ / _` \ \/ / + | |_) | (_| |> < + | .__/ \__, /_/\_\ + | | __/ | + |_| |___/ diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..254e9e1 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,64 @@ +use clap::{Args, Parser, Subcommand, builder::styling}; + +const STYLES: styling::Styles = styling::Styles::styled() + .header(styling::AnsiColor::Green.on_default().bold()) + .usage(styling::AnsiColor::Green.on_default().bold()) + .literal(styling::AnsiColor::Blue.on_default().bold()) + .placeholder(styling::AnsiColor::Cyan.on_default()); + +#[derive(Parser)] +#[command(name = "pgx")] +#[command(about = "Project-scoped PostgreSQL instance manager", long_about = None)] +#[command(version)] +#[command(styles = STYLES)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, + + #[arg(short, long, global = true)] + pub verbose: bool, +} + +#[derive(Clone, clap::ValueEnum)] +pub enum ConnectionFormat { + /// Human-readable text format + Text, + /// JSON format + Json, + /// Environment variable format + Env, +} + +#[derive(Subcommand)] +pub enum ControlCommands { + /// Start postgres instance + Start, + /// Stop postgres instance + Stop, + /// Restart postgres instance + Restart, + /// (WARNING!) Destroy postgres instance + Destroy, + /// Status of instance + Status, + /// View logs produced by postgres + Logs { follow: bool }, + /// (Sensitive) get connection details + Connection { format: ConnectionFormat }, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Create a new project, or initialize instance for existing one + Init, + /// Create a new project, or initialize instance for existing one + Sync, + + /// Start the PostgreSQL container for the current project + Instance { + // Name of the instance you want to control. Defaults to current project + name: Option, + #[command(subcommand)] + cmd: ControlCommands, + }, +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..dd012bb --- /dev/null +++ b/src/config.rs @@ -0,0 +1,174 @@ +use miette::miette; +use miette::{Context, IntoDiagnostic, Result}; +use serde::{Deserialize, Serialize}; +use serde_with::{DisplayFromStr, serde_as}; +use std::fmt::Display; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct PostgresVersion { + pub major: u32, + pub minor: u32, +} + +impl FromStr for PostgresVersion { + type Err = miette::Report; + + fn from_str(s: &str) -> std::result::Result { + let Some((major_str, minor_str)) = s.split_once(".") else { + return Err(miette!( + help = "update hardcoded version", + "expected two fragments in version" + )); + }; + let major = major_str.parse().into_diagnostic()?; + let minor = minor_str.parse().into_diagnostic()?; + Ok(Self { major, minor }) + } +} +impl Display for PostgresVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + +const PROJECT_FILENAME: &'static str = "pgx.toml"; + +/// Configuration stored in pgx.toml +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PgxConfig { + /// PostgreSQL version to use + #[serde_as(as = "DisplayFromStr")] + pub version: PostgresVersion, + + /// Database password + pub password: String, + + /// Port to bind on host + pub port: u16, +} + +impl PgxConfig { + pub fn load(path: impl AsRef) -> Result { + let path = path.as_ref(); + let content = std::fs::read_to_string(path) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read config file: {}", path.display()))?; + + let config: PgxConfig = toml::from_str(&content) + .into_diagnostic() + .wrap_err("Failed to parse pgx.toml")?; + + Ok(config) + } + + pub fn save(&self, path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let content = toml::to_string_pretty(self) + .into_diagnostic() + .wrap_err("Failed to serialize config")?; + + std::fs::write(path, content) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to write config file: {}", path.display()))?; + + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct Project { + /// Project name (derived from directory name) + pub name: String, + + /// Path to the project directory containing pgx.toml + pub path: PathBuf, + + pub config: PgxConfig, +} + +impl Project { + pub fn container_name(&self) -> String { + let container_name = format!( + "pgx-{}-{}", + self.name, + self.config.version.to_string().replace('.', "_") + ); + container_name + } + + /// Load a project from the current directory + pub fn load() -> Result> { + let project_path = get_project_path()?; + let config_path = project_path.join(PROJECT_FILENAME); + + if !config_path.exists() { + return Ok(None); + } + + let config = PgxConfig::load(&config_path)?; + let name = Self::extract_project_name(&project_path)?; + + Ok(Some(Project { + name, + path: project_path, + config, + })) + } + + pub fn new(config: PgxConfig) -> Result { + let project_path = get_project_path()?; + let name = Self::extract_project_name(&project_path)?; + + let this = Self { + name, + path: project_path, + config, + }; + + this.save_config()?; + + Ok(this) + } + + /// Extract project name from directory path + fn extract_project_name(path: &Path) -> Result { + path.file_name() + .and_then(|n| n.to_str()) + .map(|s| s.to_string()) + .ok_or_else(|| miette::miette!("Failed to extract project name from path")) + } + + /// Get the path to the pgx.toml file + pub fn config_path(&self) -> PathBuf { + self.path.join("pgx.toml") + } + + /// Save the current configuration + pub fn save_config(&self) -> Result<()> { + self.config.save(self.config_path()) + } +} + +fn get_project_path() -> Result { + let project_path = std::env::current_dir() + .into_diagnostic() + .wrap_err("Failed to get current directory")?; + Ok(project_path) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_project_name() { + let path = PathBuf::from("/home/user/my-project"); + let name = Project::extract_project_name(&path).unwrap(); + assert_eq!(name, "my-project"); + } +} diff --git a/src/controller.rs b/src/controller.rs new file mode 100644 index 0000000..ccb8c08 --- /dev/null +++ b/src/controller.rs @@ -0,0 +1,632 @@ +use miette::{bail, miette}; +use rand::{Rng, distr::Alphanumeric}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt::Write, pin::pin, str::FromStr}; + +use bollard::{ + Docker, + errors::Error, + query_parameters::{ + CreateContainerOptions, CreateImageOptions, InspectContainerOptions, ListImagesOptions, + ListImagesOptionsBuilder, SearchImagesOptions, StartContainerOptions, StopContainerOptions, + }, + secret::{ContainerConfig, ContainerCreateBody, CreateImageInfo}, +}; +use futures::{Stream, StreamExt, TryStreamExt}; +use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle}; +use miette::{Context, IntoDiagnostic, Result, diagnostic}; +use tracing::info; + +use crate::{ + config::{PgxConfig, PostgresVersion, Project}, + state::{InstanceState, StateManager}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ContainerStatus { + Running, + Stopped, + Paused, + Restarting, + Dead, + Unknown, +} + +const DOCKERHUB_POSTGRES: &str = "postgres"; +const DEFAULT_POSTGRES_PORT: u16 = 5432; +const PORT_SEARCH_RANGE: u16 = 100; + +fn format_image(ver: &PostgresVersion) -> String { + format!("{DOCKERHUB_POSTGRES}:{}", ver.to_string()) +} + +fn find_available_port() -> Result { + use std::net::TcpListener; + + for port in DEFAULT_POSTGRES_PORT..(DEFAULT_POSTGRES_PORT + PORT_SEARCH_RANGE) { + if TcpListener::bind(("127.0.0.1", port)).is_ok() { + return Ok(port); + } + } + + miette::bail!( + "No available ports found in range {}-{}", + DEFAULT_POSTGRES_PORT, + DEFAULT_POSTGRES_PORT + PORT_SEARCH_RANGE - 1 + ) +} + +fn new_download_pb(multi: &MultiProgress, layer_id: &str) -> ProgressBar { + let pb = multi.add(ProgressBar::new(0)); + pb.set_style( + ProgressStyle::with_template(&format!( + "{{spinner:.green}} [{{elapsed_precise}}] {{msg}} [{{wide_bar:.cyan/blue}}] {{bytes}}/{{total_bytes}} ({{eta}})" + )) + .unwrap() + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { + write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() + }) + .progress_chars("#>-"), + ); + pb.set_message(format!("Layer {}", layer_id)); + pb +} + +// sadly type ... = impl ... is unstable +pub async fn perform_download( + multi: MultiProgress, + chunks: impl Stream>, +) -> Result<()> { + let mut chunks = pin!(chunks); + let mut layer_progress: HashMap = HashMap::new(); + + while let Some(download_info) = chunks.try_next().await.into_diagnostic()? { + download_check_for_error(&mut layer_progress, &download_info)?; + + let layer_id = download_info.id.as_deref().unwrap_or("unknown"); + + // Get or create progress bar for this layer + let pb = layer_progress + .entry(layer_id.to_string()) + .or_insert_with(|| new_download_pb(&multi, layer_id)); + + download_drive_progress(pb, download_info); + } + + // Clean up any remaining progress bars + for (_, pb) in layer_progress.drain() { + pb.finish_and_clear(); + } + + Ok(()) +} + +fn download_drive_progress(pb: &mut ProgressBar, download_info: CreateImageInfo) { + match download_info.progress_detail { + Some(info) => match (info.current, info.total) { + (None, None) => { + pb.inc(1); + } + (current, total) => { + if let Some(total) = total { + pb.set_length(total as u64); + } + if let Some(current) = current { + pb.set_position(current as u64); + } + + if let (Some(current), Some(total)) = (current, total) + && (current == total) + { + pb.finish_with_message("Completed!"); + } + } + }, + None => { + // No progress detail, just show activity + pb.tick(); + } + } +} + +fn download_check_for_error( + layer_progress: &mut HashMap, + download_info: &CreateImageInfo, +) -> Result<()> { + if let Some(error_detail) = &download_info.error_detail { + for (_, pb) in layer_progress.drain() { + pb.finish_and_clear(); + } + + match (error_detail.code, &error_detail.message) { + (None, Some(msg)) => miette::bail!("docker image download error: {}", msg), + (Some(code), None) => miette::bail!("docker image download error: code {}", code), + (Some(code), Some(msg)) => { + miette::bail!( + "docker image download error: code {}, message: {}", + code, + msg + ) + } + _ => (), + } + } + + Ok(()) +} + +pub struct DockerController { + daemon: Docker, +} + +impl DockerController { + pub async fn new() -> Result { + let docker = Docker::connect_with_local_defaults() + .into_diagnostic() + .wrap_err( + "Failed to connect to Docker! pgx required Docker installed. Make sure it's running.", + )?; + + info!("docker.created"); + + docker + .list_images(Some(ListImagesOptions::default())) + .await + .into_diagnostic() + .wrap_err("Docker basic connectivity test refused")?; + + Ok(Self { daemon: docker }) + } + + pub async fn download_image(&self, image: String) -> Result<()> { + let options = Some(CreateImageOptions { + from_image: Some(image.clone()), + ..Default::default() + }); + + let download_progress = self.daemon.create_image(options, None, None); + + let multi = MultiProgress::new(); + + println!("Downloading {image}"); + + perform_download(multi, download_progress).await?; + + println!("Download complete!"); + + Ok(()) + } + + pub async fn ensure_version_downloaded(&self, ver: &PostgresVersion) -> Result<()> { + let desired_image_tag = format_image(ver); + + let images = self + .daemon + .list_images(Some(ListImagesOptions::default())) + .await + .into_diagnostic() + .wrap_err("failed to list installed docker images")?; + + let is_downloaded = images + .iter() + .any(|img| img.repo_tags.contains(&desired_image_tag)); + + if !is_downloaded { + self.download_image(desired_image_tag).await?; + } + + Ok(()) + } + + // TODO: make client to get available versions from dockerhub + pub async fn available_versions(&self) -> Result> { + Ok(vec!["18.1", "17.7", "16.11", "15.15", "14.20"] + .into_iter() + .map(|v| PostgresVersion::from_str(v).unwrap()) + .collect()) + } + + pub async fn container_exists(&self, container_id: &str) -> Result { + match self + .daemon + .inspect_container(container_id, None::) + .await + { + Ok(_) => Ok(true), + Err(bollard::errors::Error::DockerResponseServerError { + status_code: 404, .. + }) => Ok(false), + Err(e) => Err(e) + .into_diagnostic() + .wrap_err("Failed to inspect container"), + } + } + + pub async fn is_container_running(&self, container_name: &str) -> Result { + let container = self + .daemon + .inspect_container(container_name, None::) + .await + .into_diagnostic() + .wrap_err("Failed to inspect container")?; + + Ok(container.state.and_then(|s| s.running).unwrap_or(false)) + } + + pub async fn create_postgres_container( + &self, + container_name: &str, + version: &PostgresVersion, + password: &str, + port: u16, + ) -> Result { + use bollard::models::{HostConfig, PortBinding}; + use std::collections::HashMap; + + let image = format_image(version); + + let env = vec![ + format!("POSTGRES_PASSWORD={}", password), + format!("POSTGRES_USER={}", USERNAME), + format!("POSTGRES_DB={}", DATABASE), + ]; + + let mut port_bindings = HashMap::new(); + port_bindings.insert( + "5432/tcp".to_string(), + Some(vec![PortBinding { + host_ip: Some("127.0.0.1".to_string()), + host_port: Some(port.to_string()), + }]), + ); + + let host_config = HostConfig { + port_bindings: Some(port_bindings), + ..Default::default() + }; + + let mut labels = HashMap::new(); + labels.insert("pgx.postgres.version".to_string(), version.to_string()); + + let config = ContainerCreateBody { + image: Some(image), + env: Some(env), + host_config: Some(host_config), + labels: Some(labels), + ..Default::default() + }; + + let options = CreateContainerOptions { + name: Some(container_name.to_owned()), + platform: String::new(), + }; + + let response = self + .daemon + .create_container(Some(options), config) + .await + .into_diagnostic() + .wrap_err("Failed to create container")?; + + Ok(response.id) + } + + pub async fn start_container(&self, container_id: &str) -> Result<()> { + self.daemon + .start_container(container_id, None::) + .await + .into_diagnostic() + .wrap_err("Failed to start container")?; + + Ok(()) + } + + pub async fn container_exists_by_id(&self, container_id: &str) -> Result { + match self + .daemon + .inspect_container(container_id, None::) + .await + { + Ok(_) => Ok(true), + Err(bollard::errors::Error::DockerResponseServerError { + status_code: 404, .. + }) => Ok(false), + Err(e) => Err(e) + .into_diagnostic() + .wrap_err("Failed to inspect container by ID"), + } + } + + pub async fn is_container_running_by_id(&self, container_id: &str) -> Result { + let container = self + .daemon + .inspect_container(container_id, None::) + .await + .into_diagnostic() + .wrap_err("Failed to inspect container")?; + + Ok(container.state.and_then(|s| s.running).unwrap_or(false)) + } + + pub async fn start_container_by_id(&self, container_id: &str) -> Result<()> { + self.start_container(container_id).await + } + + pub async fn stop_container(&self, container_id: &str, timeout: i32) -> Result<()> { + self.daemon + .stop_container( + container_id, + Some(StopContainerOptions { + t: Some(timeout), + signal: None, + }), + ) + .await + .into_diagnostic() + .wrap_err("Failed to stop container")?; + + Ok(()) + } + + pub async fn get_container_postgres_version( + &self, + container_id: &str, + ) -> Result { + let container = self + .daemon + .inspect_container(container_id, None::) + .await + .into_diagnostic() + .wrap_err("Failed to inspect container")?; + + let labels = container + .config + .and_then(|c| c.labels) + .ok_or_else(|| miette!("Container has no labels"))?; + + let version_str = labels + .get("pgx.postgres.version") + .ok_or_else(|| miette!("Container missing pgx.postgres.version label"))?; + + PostgresVersion::from_str(version_str) + .map_err(|_| miette!("Invalid version in label: {}", version_str)) + } +} + +const USERNAME: &str = "postgres"; +const DATABASE: &str = "postgres"; + +const PASSWORD_LENGTH: usize = 16; +pub fn generate_password() -> String { + let password = (&mut rand::rng()) + .sample_iter(Alphanumeric) + .take(PASSWORD_LENGTH) + .map(|b| b as char) + .collect(); + password +} + +const MAX_RETRIES: u32 = 10; +const VERIFY_DURATION_SECS: u64 = 5; + +pub struct Controller { + pub docker: DockerController, + project: Option, + state: StateManager, +} + +impl Controller { + pub async fn new() -> Result { + Ok(Self { + docker: DockerController::new().await?, + project: Project::load()?, + state: StateManager::load()?, + }) + } + + pub async fn init_project(&self) -> Result<()> { + if let Some(project) = &self.project { + return self.reconcile(project).await; + } + + println!("Initializing new pgx project..."); + + let mut versions = self.docker.available_versions().await?; + versions.sort(); + let latest_version = versions + .last() + .ok_or(miette!("expected to have at least one version"))?; + + let config = PgxConfig { + version: *latest_version, + password: generate_password(), + port: find_available_port()?, + }; + let project = Project::new(config)?; + + println!("Created pgx.toml in {}", project.path.display()); + println!(" Project: {}", project.name); + println!(" PostgreSQL version: {}", project.config.version); + println!(" Port: {}", project.config.port); + println!(" Password: {}", "*".repeat(project.config.password.len())); + + self.reconcile(&project).await?; + + println!("\nProject initialized successfully!"); + + Ok(()) + } + + pub async fn reconcile(&self, project: &Project) -> Result<()> { + self.docker + .ensure_version_downloaded(&project.config.version) + .await?; + + self.ensure_container_running(project).await?; + + Ok(()) + } + + async fn ensure_container_running(&self, project: &Project) -> Result<()> { + let mut state = StateManager::load()?; + let instance_state = state.get_mut(&project.name); + + let container_id = match instance_state { + Some(instance) => match self.ensure_container_exists(&instance).await? { + Some(id) => id, + None => self.update_project_container(project, &mut state).await?, + }, + None => self.update_project_container(project, &mut state).await?, + }; + + let container_version = self + .docker + .get_container_postgres_version(&container_id) + .await?; + + self.ensure_matches_project_version(project, &mut state, &container_id, container_version) + .await?; + + if self + .docker + .is_container_running_by_id(&container_id) + .await? + { + println!("Container is already running"); + return Ok(()); + } + + println!("Starting container..."); + + for attempt in 1..=MAX_RETRIES { + let result = self.try_starting_container(&container_id, attempt).await; + + match result { + Ok(_) => break, + Err(err) => println!("Error: {:#?}", err), + } + + if attempt < MAX_RETRIES { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + } + + miette::bail!("Failed to start container after {} attempts", MAX_RETRIES) + } + + async fn try_starting_container( + &self, + container_id: &String, + attempt: u32, + ) -> Result<(), miette::Error> { + Ok( + match self.docker.start_container_by_id(container_id).await { + Ok(_) => { + tokio::time::sleep(tokio::time::Duration::from_secs(VERIFY_DURATION_SECS)) + .await; + + if self.docker.is_container_running_by_id(container_id).await? { + println!("Container started successfully and verified running"); + + return Ok(()); + } else { + println!( + "Container stopped unexpectedly after start (attempt {}/{})", + attempt, MAX_RETRIES + ); + } + } + Err(e) => { + println!( + "Failed to start container (attempt {}/{}): {}", + attempt, MAX_RETRIES, e + ); + } + }, + ) + } + + async fn update_project_container( + &self, + project: &Project, + state: &mut StateManager, + ) -> Result { + println!("Creating container {}...", project.container_name()); + let id = self + .docker + .create_postgres_container( + &project.container_name(), + &project.config.version, + &project.config.password, + project.config.port, + ) + .await?; + println!("Container created successfully"); + state.set( + project.name.clone(), + crate::state::InstanceState::new( + id.clone(), + project.config.version.to_string(), + DATABASE.to_string(), + USERNAME.to_string(), + project.config.port, + ), + ); + state.save()?; + Ok(id) + } + + async fn ensure_container_exists( + &self, + instance: &InstanceState, + ) -> Result, miette::Error> { + let mut container_id = None; + let id = &instance.container_id; + if self.docker.container_exists_by_id(id).await? { + container_id = Some(id.clone()); + } + Ok(container_id) + } + + async fn ensure_matches_project_version( + &self, + project: &Project, + _state: &mut StateManager, + _container_id: &String, + container_version: PostgresVersion, + ) -> Result<(), miette::Error> { + Ok(if container_version != project.config.version { + let needs_upgrade = container_version < project.config.version; + + if needs_upgrade { + bail!("Upgrades are currently unsupported! :("); + // println!( + // "Upgrading PostgreSQL from {} to {}...", + // container_version, project.config.version + // ); + // self.docker.stop_container(container_id, 10).await?; + // self.docker + // .upgrade_container_image( + // container_id, + // container_name, + // &project.config.version, + // &project.config.password, + // project.config.port, + // ) + // .await?; + + // if let Some(instance_state) = state.get_mut(&project.name) { + // instance_state.postgres_version = project.config.version.to_string(); + // state.save()?; + // } + } else { + miette::bail!( + "Cannot downgrade PostgreSQL from {} to {}. Downgrades are not supported.", + container_version, + project.config.version + ); + } + }) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..044a597 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,39 @@ +mod cli; +mod config; +mod state; + +mod controller; + +use clap::Parser; +use cli::Cli; +use miette::Result; +use tracing::info; + +use crate::controller::Controller; + +#[tokio::main] +async fn main() -> Result<()> { + println!("{}", include_str!("./banner.txt")); + let controller = Controller::new().await?; + + let cli = Cli::parse(); + init_tracing(cli.verbose); + + info!("pgx.start"); + + match cli.command { + cli::Commands::Init => controller.init_project().await?, + cli::Commands::Instance { name, cmd } => todo!(), + cli::Commands::Sync => todo!(), + } + + Ok(()) +} + +fn init_tracing(verbose: bool) { + use tracing_subscriber::{fmt, prelude::*}; + + tracing_subscriber::registry() + .with(fmt::layer().with_target(false).with_level(true)) + .init(); +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..b1a1fe6 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,203 @@ +use miette::{Context, IntoDiagnostic, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +/// State information for a single PostgreSQL instance +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InstanceState { + /// Docker container ID + pub container_id: String, + + /// PostgreSQL version running in the container + pub postgres_version: String, + + /// Database name + pub database_name: String, + + /// User name + pub user_name: String, + + /// Port the container is bound to + pub port: u16, + + /// Timestamp when the instance was created (Unix timestamp) + pub created_at: u64, + + /// Timestamp when the instance was last started (Unix timestamp) + pub last_started_at: Option, +} + +/// Manages the global state file at ~/.pgx/state.json +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StateManager { + /// Map of project name to instance state + #[serde(default)] + instances: HashMap, +} + +/// Get the path to the state file (~/.pgx/state.json) + +fn state_file_path() -> Result { + let home = std::env::var("HOME") + .into_diagnostic() + .wrap_err("Failed to get HOME environment variable")?; + + Ok(PathBuf::from(home).join(".pgx").join("state.json")) +} + +/// Get the path to the .pgx directory +pub fn pgx_dir() -> Result { + let home = std::env::var("HOME") + .into_diagnostic() + .wrap_err("Failed to get HOME environment variable")?; + + Ok(PathBuf::from(home).join(".pgx")) +} + +impl StateManager { + /// Load the state manager from disk, or create a new one if it doesn't exist + pub fn load() -> Result { + let state_path = state_file_path()?; + + if !state_path.exists() { + // Create the directory if it doesn't exist + if let Some(parent) = state_path.parent() { + std::fs::create_dir_all(parent) + .into_diagnostic() + .wrap_err("Failed to create .pgx directory")?; + } + + // Return empty state + return Ok(StateManager { + instances: HashMap::new(), + }); + } + + let content = std::fs::read_to_string(&state_path) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read state file: {}", state_path.display()))?; + + let state: StateManager = serde_json::from_str(&content) + .into_diagnostic() + .wrap_err("Failed to parse state.json")?; + + Ok(state) + } + + /// Save the state manager to disk + pub fn save(&self) -> Result<()> { + let state_path = state_file_path()?; + + // Ensure directory exists + if let Some(parent) = state_path.parent() { + std::fs::create_dir_all(parent) + .into_diagnostic() + .wrap_err("Failed to create .pgx directory")?; + } + + let content = serde_json::to_string_pretty(self) + .into_diagnostic() + .wrap_err("Failed to serialize state")?; + + std::fs::write(&state_path, content) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to write state file: {}", state_path.display()))?; + + Ok(()) + } + + /// Get the state for a specific project + pub fn get(&self, project_name: &str) -> Option<&InstanceState> { + self.instances.get(project_name) + } + + /// Get mutable state for a specific project + pub fn get_mut(&mut self, project_name: &str) -> Option<&mut InstanceState> { + self.instances.get_mut(project_name) + } + + /// Set the state for a specific project + pub fn set(&mut self, project_name: String, state: InstanceState) { + self.instances.insert(project_name, state); + } + + /// Update the state for a specific project, creating it if it doesn't exist + pub fn update(&mut self, project_name: &str, updater: F) -> Result<()> + where + F: FnOnce(&mut InstanceState), + { + if let Some(state) = self.instances.get_mut(project_name) { + updater(state); + Ok(()) + } else { + miette::bail!("No state found for project: {}", project_name) + } + } + + /// Remove the state for a specific project + pub fn remove(&mut self, project_name: &str) -> Option { + self.instances.remove(project_name) + } + + /// Get all instances + pub fn all_instances(&self) -> &HashMap { + &self.instances + } +} + +impl InstanceState { + pub fn new( + container_id: String, + postgres_version: String, + database_name: String, + user_name: String, + port: u16, + ) -> Self { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + InstanceState { + container_id, + postgres_version, + database_name, + user_name, + port, + created_at: now, + last_started_at: Some(now), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_state_manager_operations() { + let mut manager = StateManager { + instances: HashMap::new(), + }; + + let state = InstanceState::new( + "container123".to_string(), + "16".to_string(), + "mydb".to_string(), + "postgres".to_string(), + 5432, + ); + + manager.set("my-project".to_string(), state); + + assert!(manager.get("my-project").is_some()); + assert_eq!( + manager.get("my-project").unwrap().container_id, + "container123" + ); + + manager.remove("my-project"); + assert!(manager.get("my-project").is_none()); + } +}