From b451f72bec62ab6d9abcaee4c90ca43049674036 Mon Sep 17 00:00:00 2001 From: Paul Thurlow Date: Mon, 30 Mar 2026 12:53:43 -0700 Subject: [PATCH 1/2] clean up connections commands and include getter --- README.md | 13 ++--- skills/hotdata-cli/SKILL.md | 6 ++- src/command.rs | 96 ++++--------------------------------- src/connections.rs | 30 ++++++++++++ src/main.rs | 76 ++++++++++++++++------------- 5 files changed, 91 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 9ce9fd6..cc9a5dc 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,8 @@ API key priority (lowest to highest): config file → `HOTDATA_API_KEY` env var | Command | Subcommands | Description | | :-- | :-- | :-- | | `auth` | `status`, `logout` | Authenticate (run without subcommand to log in) | -| `workspaces` | `list`, `set`, `get`, `create`, `update` | Manage workspaces | -| `connections` | `list`, `get`, `create`, `refresh`, `update`, `delete`, `new` | Manage connections | +| `workspaces` | `list`, `set` | Manage workspaces | +| `connections` | `list`, `create`, `refresh`, `new` | Manage connections | | `tables` | `list` | List tables and columns | | `datasets` | `list`, `create` | Manage uploaded datasets | | `query` | | Execute a SQL query | @@ -85,13 +85,14 @@ hotdata workspaces set [] ## Connections ```sh -hotdata connections list [--workspace-id ] [--format table|json|yaml] -hotdata connections get [--workspace-id ] [--format yaml|json|table] -hotdata connections refresh [--workspace-id ] -hotdata connections new [--workspace-id ] +hotdata connections list [-w ] [-o table|json|yaml] +hotdata connections [-w ] [-o table|json|yaml] +hotdata connections refresh [-w ] +hotdata connections new [-w ] ``` - `list` returns `id`, `name`, `source_type` for each connection. +- Pass a connection ID to view details (id, name, source type, table counts). - `refresh` triggers a schema refresh for a connection. - `new` launches an interactive connection creation wizard. diff --git a/skills/hotdata-cli/SKILL.md b/skills/hotdata-cli/SKILL.md index 50ac800..7d94e2a 100644 --- a/skills/hotdata-cli/SKILL.md +++ b/skills/hotdata-cli/SKILL.md @@ -39,9 +39,11 @@ Returns workspaces with `public_id`, `name`, `active`, `favorite`, `provision_st ### List Connections ``` -hotdata connections list [--workspace-id ] [--format table|json|yaml] +hotdata connections list [-w ] [-o table|json|yaml] +hotdata connections [-w ] [-o table|json|yaml] ``` -Returns `id`, `name`, `source_type` for each connection in the workspace. +- `list` returns `id`, `name`, `source_type` for each connection. +- Pass a connection ID to view details (id, name, source type, table counts). ### Create a Connection diff --git a/src/command.rs b/src/command.rs index b6e6a41..1e9cb43 100644 --- a/src/command.rs +++ b/src/command.rs @@ -51,12 +51,19 @@ pub enum Commands { /// Manage workspace connections Connections { + /// Connection ID to show details + id: Option, + /// Workspace ID (defaults to first workspace from login) #[arg(long, short = 'w', global = true)] workspace_id: Option, + /// Output format (used with connection ID) + #[arg(long = "output", short = 'o', default_value = "table", value_parser = ["table", "json", "yaml"])] + output: String, + #[command(subcommand)] - command: ConnectionsCommands, + command: Option, }, /// Manage tables in a workspace @@ -351,55 +358,6 @@ pub enum WorkspaceCommands { /// Workspace ID to set as default (omit for interactive selection) workspace_id: Option, }, - - /// Get details for a workspace - Get { - /// Workspace ID (defaults to first workspace from login) - #[arg(long, short = 'w')] - workspace_id: Option, - - /// Output format - #[arg(long = "output", short = 'o', default_value = "yaml", value_parser = ["table", "json", "yaml"])] - output: String, - }, - - /// Create a new workspace - Create { - /// Workspace name - #[arg(long)] - name: String, - - /// Workspace description - #[arg(long, default_value = "")] - description: String, - - /// Organization ID for the workspace - #[arg(long)] - organization_id: String, - - /// Output format - #[arg(long = "output", short = 'o', default_value = "yaml", value_parser = ["table", "json", "yaml"])] - output: String, - }, - - /// Update an existing workspace - Update { - /// Workspace ID (defaults to first workspace from login) - #[arg(long, short = 'w')] - workspace_id: Option, - - /// New workspace name - #[arg(long)] - name: Option, - - /// New workspace description - #[arg(long)] - description: Option, - - /// Output format - #[arg(long = "output", short = 'o', default_value = "yaml", value_parser = ["table", "json", "yaml"])] - output: String, - }, } #[derive(Subcommand)] @@ -427,16 +385,6 @@ pub enum ConnectionsCommands { output: String, }, - /// Get details for a specific connection - Get { - /// Connection ID - connection_id: String, - - /// Output format - #[arg(long = "output", short = 'o', default_value = "yaml", value_parser = ["table", "json", "yaml"])] - output: String, - }, - /// Create a new connection, or list/inspect available connection types Create { #[command(subcommand)] @@ -459,39 +407,11 @@ pub enum ConnectionsCommands { output: String, }, - /// Update a connection in a workspace - Update { - /// Connection ID - connection_id: String, - - /// New connection name - #[arg(long)] - name: Option, - - /// New connection type - #[arg(long = "type")] - conn_type: Option, - - /// New connection config as JSON string - #[arg(long)] - config: Option, - - /// Output format - #[arg(long = "output", short = 'o', default_value = "yaml", value_parser = ["table", "json", "yaml"])] - output: String, - }, - /// Refresh a connection's schema Refresh { /// Connection ID connection_id: String, }, - - /// Delete a connection from a workspace - Delete { - /// Connection ID - connection_id: String, - }, } #[derive(Subcommand)] diff --git a/src/connections.rs b/src/connections.rs index 8168c9f..ba8e43b 100644 --- a/src/connections.rs +++ b/src/connections.rs @@ -70,11 +70,41 @@ struct Connection { source_type: String, } +#[derive(Deserialize, Serialize)] +struct ConnectionDetail { + id: String, + name: String, + source_type: String, + #[serde(default)] + table_count: u64, + #[serde(default)] + synced_table_count: u64, +} + #[derive(Deserialize)] struct ListResponse { connections: Vec, } +pub fn get(workspace_id: &str, connection_id: &str, format: &str) { + let api = ApiClient::new(Some(workspace_id)); + let detail: ConnectionDetail = api.get(&format!("/connections/{connection_id}")); + + match format { + "json" => println!("{}", serde_json::to_string_pretty(&detail).unwrap()), + "yaml" => print!("{}", serde_yaml::to_string(&detail).unwrap()), + "table" => { + use crossterm::style::Stylize; + let label = |l: &str| format!("{:<16}", l).dark_grey().to_string(); + println!("{}{}", label("id:"), detail.id.dark_cyan()); + println!("{}{}", label("name:"), detail.name.white()); + println!("{}{}", label("source_type:"), detail.source_type.green()); + println!("{}{}", label("tables:"), format!("{} synced / {} total", detail.synced_table_count.to_string().cyan(), detail.table_count.to_string().cyan())); + } + _ => unreachable!(), + } +} + pub fn create( workspace_id: &str, name: &str, diff --git a/src/main.rs b/src/main.rs index 0756348..9d2e696 100644 --- a/src/main.rs +++ b/src/main.rs @@ -116,47 +116,55 @@ fn main() { Commands::Workspaces { command } => match command { WorkspaceCommands::List { output } => workspace::list(&output), WorkspaceCommands::Set { workspace_id } => workspace::set(workspace_id.as_deref()), - _ => eprintln!("not yet implemented"), }, - Commands::Connections { workspace_id, command } => { + Commands::Connections { id, workspace_id, output, command } => { let workspace_id = resolve_workspace(workspace_id); - match command { - ConnectionsCommands::New => connections_new::run(&workspace_id), - ConnectionsCommands::List { output } => { - connections::list(&workspace_id, &output) - } - ConnectionsCommands::Create { command, name, source_type, config, output } => { - match command { - Some(ConnectionsCreateCommands::List { name, output }) => { - match name.as_deref() { - Some(name) => connections::types_get(&workspace_id, name, &output), - None => connections::types_list(&workspace_id, &output), + if let Some(id) = id { + connections::get(&workspace_id, &id, &output) + } else { + match command { + Some(ConnectionsCommands::New) => connections_new::run(&workspace_id), + Some(ConnectionsCommands::List { output }) => { + connections::list(&workspace_id, &output) + } + Some(ConnectionsCommands::Create { command, name, source_type, config, output }) => { + match command { + Some(ConnectionsCreateCommands::List { name, output }) => { + match name.as_deref() { + Some(name) => connections::types_get(&workspace_id, name, &output), + None => connections::types_list(&workspace_id, &output), + } } - } - None => { - let missing: Vec<&str> = [ - name.is_none().then_some("--name"), - source_type.is_none().then_some("--type"), - config.is_none().then_some("--config"), - ].into_iter().flatten().collect(); - if !missing.is_empty() { - eprintln!("error: missing required arguments: {}", missing.join(", ")); - std::process::exit(1); + None => { + let missing: Vec<&str> = [ + name.is_none().then_some("--name"), + source_type.is_none().then_some("--type"), + config.is_none().then_some("--config"), + ].into_iter().flatten().collect(); + if !missing.is_empty() { + eprintln!("error: missing required arguments: {}", missing.join(", ")); + std::process::exit(1); + } + connections::create( + &workspace_id, + &name.unwrap(), + &source_type.unwrap(), + &config.unwrap(), + &output, + ) } - connections::create( - &workspace_id, - &name.unwrap(), - &source_type.unwrap(), - &config.unwrap(), - &output, - ) } } + Some(ConnectionsCommands::Refresh { connection_id }) => { + connections::refresh(&workspace_id, &connection_id) + } + None => { + use clap::CommandFactory; + let mut cmd = Cli::command(); + cmd.build(); + cmd.find_subcommand_mut("connections").unwrap().print_help().unwrap(); + } } - ConnectionsCommands::Refresh { connection_id } => { - connections::refresh(&workspace_id, &connection_id) - } - _ => eprintln!("not yet implemented"), } }, Commands::Tables { command } => match command { From 0f6438d1f44eaf3d0a6459c688d6895e56088c1c Mon Sep 17 00:00:00 2001 From: Paul Thurlow Date: Mon, 30 Mar 2026 13:02:39 -0700 Subject: [PATCH 2/2] modify workflow for github app deployment --- .github/workflows/publish-homebrew.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-homebrew.yml b/.github/workflows/publish-homebrew.yml index 7cc0d6c..15e3a42 100644 --- a/.github/workflows/publish-homebrew.yml +++ b/.github/workflows/publish-homebrew.yml @@ -11,17 +11,23 @@ jobs: publish-homebrew-formula: runs-on: ubuntu-22.04 env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PLAN: ${{ inputs.plan }} - GITHUB_USER: "axo bot" - GITHUB_EMAIL: "admin+bot@axo.dev" if: ${{ !fromJson(inputs.plan).announcement_is_prerelease || fromJson(inputs.plan).publish_prereleases }} steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HOTDATA_AUTOMATION_APP_ID }} + private-key: ${{ secrets.HOTDATA_AUTOMATION_PRIVATE_KEY }} + owner: hotdata-dev + repositories: homebrew-tap + - uses: actions/checkout@v6 with: persist-credentials: true repository: "hotdata-dev/homebrew-tap" - token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + token: ${{ steps.app-token.outputs.token }} - name: Fetch homebrew formulae uses: actions/download-artifact@v7 @@ -32,8 +38,8 @@ jobs: - name: Patch and commit formula files run: | - git config --global user.name "${GITHUB_USER}" - git config --global user.email "${GITHUB_EMAIL}" + git config --global user.name "hotdata-automation[bot]" + git config --global user.email "hotdata-automation[bot]@users.noreply.github.com" for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) @@ -55,6 +61,8 @@ jobs: git push - name: Remove .rb from GitHub Release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG=$(echo "$PLAN" | jq -r '.announcement_tag') if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then