Compare commits
5 Commits
d39f67f3fe
...
bc37b58d80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc37b58d80 | ||
|
|
bf0ae1b3ee | ||
|
|
ddb28085b9 | ||
|
|
71f363fde9 | ||
|
|
6e2ee650d8 |
24
CLAUDE.md
24
CLAUDE.md
@@ -4,27 +4,27 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 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.
|
||||
`pgd` 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 `pgd.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 has a `pgd.toml` file at its root that defines the Postgres configuration
|
||||
- The project name is derived from the directory containing `pgd.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:
|
||||
The `pgd.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.
|
||||
Values are auto-populated during `pgd init` with sensible defaults or random values where appropriate.
|
||||
|
||||
### State Tracking
|
||||
|
||||
@@ -36,7 +36,7 @@ The tool maintains separate state for each instance to detect configuration drif
|
||||
## Key Dependencies
|
||||
|
||||
- **clap** (with derive feature): CLI argument parsing and command structure
|
||||
- **toml**: Parsing and serializing `pgx.toml` configuration files
|
||||
- **toml**: Parsing and serializing `pgd.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
|
||||
@@ -80,15 +80,15 @@ cargo fmt -- --check # Check without modifying
|
||||
|
||||
The CLI follows this pattern:
|
||||
```
|
||||
pgx <command> [options]
|
||||
pgd <command> [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
|
||||
- `pgd init`: Create pgd.toml with auto-populated configuration
|
||||
- `pgd start`: Start the Postgres container for current project
|
||||
- `pgd stop`: Stop the running container
|
||||
- `pgd status`: Show instance status and detect drift
|
||||
- `pgd destroy`: Remove container and clean up
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
|
||||
155
Cargo.lock
generated
155
Cargo.lock
generated
@@ -255,6 +255,26 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "comfy-table"
|
||||
version = "7.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.1"
|
||||
@@ -274,6 +294,29 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"document-features",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.21.3"
|
||||
@@ -330,6 +373,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
@@ -849,6 +901,12 @@ version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -1001,11 +1059,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pgx"
|
||||
version = "0.1.0"
|
||||
name = "pgd"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bollard",
|
||||
"clap",
|
||||
"colored",
|
||||
"comfy-table",
|
||||
"futures",
|
||||
"indicatif",
|
||||
"miette",
|
||||
@@ -1876,13 +1936,22 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1894,6 +1963,22 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
@@ -1901,58 +1986,106 @@ 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",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
[package]
|
||||
name = "pgx"
|
||||
version = "0.1.0"
|
||||
name = "pgd"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
description = "CLI to manage postgres instances for local development"
|
||||
repository = "https://github.com/MarketTakers/pgd"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
bollard = "0.19.4"
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
colored = "3.0.0"
|
||||
comfy-table = "7.2.1"
|
||||
futures = "0.3.31"
|
||||
indicatif = { version = "0.18.3", features = ["improved_unicode"] }
|
||||
miette = { version = "7.6.0", features = ["fancy"] }
|
||||
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 MarketTakers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
3
pgd.toml
Normal file
3
pgd.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
version = "18.1"
|
||||
password = "a7BASi7P3gCgc0Xx"
|
||||
port = 5433
|
||||
@@ -1,7 +1,7 @@
|
||||
_______ _______ __ __
|
||||
| || || |_| |
|
||||
| _ || ___|| |
|
||||
| |_| || | __ | |
|
||||
| ___|| || | | |
|
||||
| | | |_| || _ |
|
||||
|___| |_______||__| |__|
|
||||
▄▄▄▄ ▐▌
|
||||
█ █ ▐▌
|
||||
█▄▄▄▀ ▗▞▀▜▌
|
||||
█ ▗▄▖▝▚▄▟▌
|
||||
▀ ▐▌ ▐▌
|
||||
▝▀▜▌
|
||||
▐▙▄▞▘
|
||||
@@ -7,7 +7,7 @@ const STYLES: styling::Styles = styling::Styles::styled()
|
||||
.placeholder(styling::AnsiColor::Cyan.on_default());
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "pgx")]
|
||||
#[command(name = "pgd")]
|
||||
#[command(about = "Project-scoped PostgreSQL instance manager", long_about = None)]
|
||||
#[command(version)]
|
||||
#[command(styles = STYLES)]
|
||||
@@ -51,8 +51,6 @@ pub enum ControlCommands {
|
||||
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 {
|
||||
|
||||
@@ -35,12 +35,12 @@ impl Display for PostgresVersion {
|
||||
}
|
||||
}
|
||||
|
||||
const PROJECT_FILENAME: &str = "pgx.toml";
|
||||
const PROJECT_FILENAME: &str = "pgd.toml";
|
||||
|
||||
/// Configuration stored in pgx.toml
|
||||
/// Configuration stored in pgd.toml
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PgxConfig {
|
||||
pub struct PGDConfig {
|
||||
/// PostgreSQL version to use
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub version: PostgresVersion,
|
||||
@@ -52,16 +52,16 @@ pub struct PgxConfig {
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl PgxConfig {
|
||||
impl PGDConfig {
|
||||
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
|
||||
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)
|
||||
let config: PGDConfig = toml::from_str(&content)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to parse pgx.toml")?;
|
||||
.wrap_err("Failed to parse pgd.toml")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
@@ -85,16 +85,16 @@ pub struct Project {
|
||||
/// Project name (derived from directory name)
|
||||
pub name: String,
|
||||
|
||||
/// Path to the project directory containing pgx.toml
|
||||
/// Path to the project directory containing pgd.toml
|
||||
pub path: PathBuf,
|
||||
|
||||
pub config: PgxConfig,
|
||||
pub config: PGDConfig,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn container_name(&self) -> String {
|
||||
let container_name = format!(
|
||||
"pgx-{}-{}",
|
||||
"pgd-{}-{}",
|
||||
self.name,
|
||||
self.config.version.to_string().replace('.', "_")
|
||||
);
|
||||
@@ -110,7 +110,7 @@ impl Project {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let config = PgxConfig::load(&config_path)?;
|
||||
let config = PGDConfig::load(&config_path)?;
|
||||
let name = Self::extract_project_name(&project_path)?;
|
||||
|
||||
Ok(Some(Project {
|
||||
@@ -120,7 +120,7 @@ impl Project {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn new(config: PgxConfig) -> Result<Self> {
|
||||
pub fn new(config: PGDConfig) -> Result<Self> {
|
||||
let project_path = get_project_path()?;
|
||||
let name = Self::extract_project_name(&project_path)?;
|
||||
|
||||
@@ -143,9 +143,9 @@ impl Project {
|
||||
.ok_or_else(|| miette::miette!("Failed to extract project name from path"))
|
||||
}
|
||||
|
||||
/// Get the path to the pgx.toml file
|
||||
/// Get the path to the pgd.toml file
|
||||
pub fn config_path(&self) -> PathBuf {
|
||||
self.path.join("pgx.toml")
|
||||
self.path.join("pgd.toml")
|
||||
}
|
||||
|
||||
/// Save the current configuration
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use miette::{bail, miette};
|
||||
|
||||
use colored::Colorize;
|
||||
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
|
||||
use miette::Result;
|
||||
|
||||
use crate::{
|
||||
config::{PgxConfig, PostgresVersion, Project},
|
||||
config::{PGDConfig, PostgresVersion, Project},
|
||||
controller::docker::DockerController,
|
||||
state::{InstanceState, StateManager},
|
||||
};
|
||||
@@ -12,11 +16,12 @@ mod docker;
|
||||
mod utils;
|
||||
|
||||
const MAX_RETRIES: u32 = 10;
|
||||
const VERIFY_DURATION_SECS: u64 = 10;
|
||||
const VERIFY_DURATION_SECS: u64 = 5;
|
||||
|
||||
pub struct Controller {
|
||||
docker: DockerController,
|
||||
project: Option<Project>,
|
||||
#[allow(unused)]
|
||||
state: StateManager,
|
||||
}
|
||||
|
||||
@@ -34,7 +39,7 @@ impl Controller {
|
||||
return self.reconcile(project).await;
|
||||
}
|
||||
|
||||
println!("Initializing new pgx project...");
|
||||
println!("{}", "Initializing new pgd project...".cyan());
|
||||
|
||||
let mut versions = self.docker.available_versions().await?;
|
||||
versions.sort();
|
||||
@@ -42,22 +47,55 @@ impl Controller {
|
||||
.last()
|
||||
.ok_or(miette!("expected to have at least one version"))?;
|
||||
|
||||
let config = PgxConfig {
|
||||
let config = PGDConfig {
|
||||
version: *latest_version,
|
||||
password: utils::generate_password(),
|
||||
port: utils::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()));
|
||||
println!(
|
||||
"\n{} {}\n",
|
||||
"Created pgd.toml in",
|
||||
project.path.display().to_string().bright_white().bold()
|
||||
);
|
||||
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.load_preset(UTF8_FULL)
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_style(comfy_table::TableComponent::MiddleIntersections, ' ')
|
||||
.set_header(vec![
|
||||
Cell::new("Instance Configuration").add_attribute(Attribute::Bold),
|
||||
]);
|
||||
|
||||
use comfy_table::TableComponent::*;
|
||||
table.set_style(TopLeftCorner, '╭');
|
||||
table.set_style(TopRightCorner, '╮');
|
||||
table.set_style(BottomLeftCorner, '╰');
|
||||
table.set_style(BottomRightCorner, '╯');
|
||||
table.add_row(vec![
|
||||
Cell::new("Project").fg(Color::White),
|
||||
Cell::new(&project.name).add_attribute(Attribute::Bold),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
Cell::new("PostgreSQL Version").fg(Color::White),
|
||||
Cell::new(project.config.version.to_string()).add_attribute(Attribute::Bold),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
Cell::new("Port").fg(Color::White),
|
||||
Cell::new(project.config.port.to_string()).add_attribute(Attribute::Bold),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
Cell::new("Password").fg(Color::White),
|
||||
Cell::new("*".repeat(project.config.password.len())).fg(Color::DarkGrey),
|
||||
]);
|
||||
|
||||
println!("{table}");
|
||||
|
||||
self.reconcile(&project).await?;
|
||||
|
||||
println!("\nProject initialized successfully!");
|
||||
println!("\n{}", "✓ Project initialized successfully!".green().bold());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -97,17 +135,46 @@ impl Controller {
|
||||
.is_container_running_by_id(&container_id)
|
||||
.await?
|
||||
{
|
||||
println!("{}", "Container is already running".white());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Starting container...");
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
spinner.enable_steady_tick(Duration::from_millis(100));
|
||||
spinner.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template("{spinner:.cyan} {msg}")
|
||||
.unwrap(),
|
||||
);
|
||||
spinner.set_message("Starting container...");
|
||||
|
||||
for attempt in 1..=MAX_RETRIES {
|
||||
let result = self.try_starting_container(&container_id, attempt).await;
|
||||
spinner.set_message(format!(
|
||||
"Starting container (attempt {}/{})",
|
||||
attempt, MAX_RETRIES
|
||||
));
|
||||
|
||||
let result = self.try_starting_container(&container_id, &spinner).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) => println!("Error: {:#?}", err),
|
||||
Ok(_) => {
|
||||
spinner.finish_with_message(format!(
|
||||
"{}",
|
||||
"Container started successfully".green().bold()
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
spinner.set_message(format!(
|
||||
"{} {}/{} failed: {}",
|
||||
"Attempt".yellow(),
|
||||
attempt,
|
||||
MAX_RETRIES,
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if attempt < MAX_RETRIES {
|
||||
@@ -115,37 +182,43 @@ impl Controller {
|
||||
}
|
||||
}
|
||||
|
||||
spinner.finish_with_message(format!("{}", "Failed to start container".red()));
|
||||
miette::bail!("Failed to start container after {} attempts", MAX_RETRIES)
|
||||
}
|
||||
|
||||
async fn try_starting_container(
|
||||
&self,
|
||||
container_id: &String,
|
||||
attempt: u32,
|
||||
spinner: &indicatif::ProgressBar,
|
||||
) -> Result<(), miette::Error> {
|
||||
match self.docker.start_container_by_id(container_id).await {
|
||||
Ok(_) => {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(VERIFY_DURATION_SECS)).await;
|
||||
spinner.set_message(format!(
|
||||
"{} ({}s)...",
|
||||
"Verifying container is running".cyan(),
|
||||
VERIFY_DURATION_SECS
|
||||
));
|
||||
|
||||
for i in 0..VERIFY_DURATION_SECS {
|
||||
spinner.set_message(format!(
|
||||
"{} ({}/{}s)",
|
||||
"Verifying container stability".cyan(),
|
||||
i + 1,
|
||||
VERIFY_DURATION_SECS
|
||||
));
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).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
|
||||
);
|
||||
miette::bail!("Container stopped unexpectedly after start");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Failed to start container (attempt {}/{}): {}",
|
||||
attempt, MAX_RETRIES, e
|
||||
);
|
||||
miette::bail!("Failed to start: {}", e);
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_project_container(
|
||||
@@ -153,7 +226,11 @@ impl Controller {
|
||||
project: &Project,
|
||||
state: &mut StateManager,
|
||||
) -> Result<String, miette::Error> {
|
||||
println!("Creating container {}...", project.container_name());
|
||||
println!(
|
||||
"{} {}",
|
||||
"Creating container".cyan(),
|
||||
project.container_name().yellow()
|
||||
);
|
||||
let id = self
|
||||
.docker
|
||||
.create_postgres_container(
|
||||
@@ -163,7 +240,7 @@ impl Controller {
|
||||
project.config.port,
|
||||
)
|
||||
.await?;
|
||||
println!("Container created successfully");
|
||||
println!("{}", "Container created successfully".green());
|
||||
state.set(
|
||||
project.name.clone(),
|
||||
crate::state::InstanceState::new(
|
||||
|
||||
@@ -9,6 +9,7 @@ use bollard::{
|
||||
},
|
||||
secret::ContainerCreateBody,
|
||||
};
|
||||
use colored::Colorize;
|
||||
use indicatif::MultiProgress;
|
||||
use miette::{Context, IntoDiagnostic, Result};
|
||||
use tracing::info;
|
||||
@@ -34,7 +35,7 @@ impl DockerController {
|
||||
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.",
|
||||
"Failed to connect to Docker! pgd required Docker installed. Make sure it's running.",
|
||||
)?;
|
||||
|
||||
info!("docker.created");
|
||||
@@ -58,11 +59,11 @@ impl DockerController {
|
||||
|
||||
let multi = MultiProgress::new();
|
||||
|
||||
println!("Downloading {image}");
|
||||
println!("{} {}", "Downloading".cyan(), image.yellow());
|
||||
|
||||
download::perform_download(multi, download_progress).await?;
|
||||
|
||||
println!("Download complete!");
|
||||
println!("{}", "Download complete!".green().bold());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -156,7 +157,7 @@ impl DockerController {
|
||||
};
|
||||
|
||||
let mut labels = HashMap::new();
|
||||
labels.insert("pgx.postgres.version".to_string(), version.to_string());
|
||||
labels.insert("pgd.postgres.version".to_string(), version.to_string());
|
||||
|
||||
let config = ContainerCreateBody {
|
||||
image: Some(image),
|
||||
@@ -255,8 +256,8 @@ impl DockerController {
|
||||
.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"))?;
|
||||
.get("pgd.postgres.version")
|
||||
.ok_or_else(|| miette!("Container missing pgd.postgres.version label"))?;
|
||||
|
||||
PostgresVersion::from_str(version_str)
|
||||
.map_err(|_| miette!("Invalid version in label: {}", version_str))
|
||||
|
||||
@@ -23,13 +23,12 @@ async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
init_tracing(cli.verbose);
|
||||
|
||||
info!("pgx.start");
|
||||
info!("pgd.start");
|
||||
let controller = Controller::new().await?;
|
||||
|
||||
match cli.command {
|
||||
cli::Commands::Init => controller.init_project().await?,
|
||||
cli::Commands::Instance { name, cmd } => todo!(),
|
||||
cli::Commands::Sync => todo!(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
75
src/state.rs
75
src/state.rs
@@ -21,7 +21,7 @@ pub struct InstanceState {
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
/// Manages the global state file at ~/.pgx/state.json
|
||||
/// Manages the global state file at ~/.pgd/state.json
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StateManager {
|
||||
/// Map of project name to instance state
|
||||
@@ -29,24 +29,16 @@ pub struct StateManager {
|
||||
instances: HashMap<String, InstanceState>,
|
||||
}
|
||||
|
||||
/// Get the path to the state file (~/.pgx/state.json)
|
||||
/// Get the path to the state file (~/.pgd/state.json)
|
||||
|
||||
fn state_file_path() -> Result<PathBuf> {
|
||||
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"))
|
||||
Ok(PathBuf::from(home).join(".pgd").join("state.json"))
|
||||
}
|
||||
|
||||
/// Get the path to the .pgx directory
|
||||
pub fn pgx_dir() -> Result<PathBuf> {
|
||||
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
|
||||
@@ -58,7 +50,7 @@ impl StateManager {
|
||||
if let Some(parent) = state_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to create .pgx directory")?;
|
||||
.wrap_err("Failed to create .pgd directory")?;
|
||||
}
|
||||
|
||||
// Return empty state
|
||||
@@ -86,7 +78,7 @@ impl StateManager {
|
||||
if let Some(parent) = state_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to create .pgx directory")?;
|
||||
.wrap_err("Failed to create .pgd directory")?;
|
||||
}
|
||||
|
||||
let content = serde_json::to_string_pretty(self)
|
||||
@@ -100,11 +92,6 @@ impl StateManager {
|
||||
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)
|
||||
@@ -115,28 +102,10 @@ impl StateManager {
|
||||
self.instances.insert(project_name, state);
|
||||
}
|
||||
|
||||
/// Update the state for a specific project, creating it if it doesn't exist
|
||||
pub fn update<F>(&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<InstanceState> {
|
||||
self.instances.remove(project_name)
|
||||
}
|
||||
|
||||
/// Get all instances
|
||||
pub fn all_instances(&self) -> &HashMap<String, InstanceState> {
|
||||
&self.instances
|
||||
}
|
||||
}
|
||||
|
||||
impl InstanceState {
|
||||
@@ -153,36 +122,4 @@ impl InstanceState {
|
||||
created_at: 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(),
|
||||
PostgresVersion {
|
||||
major: 18,
|
||||
minor: 1,
|
||||
},
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user