Skip to content

Commit

Permalink
feat: publish grit blueprints subcommands (#598)
Browse files Browse the repository at this point in the history
  • Loading branch information
morgante authored Jan 7, 2025
1 parent 4a4b212 commit 0d0319f
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 15 deletions.
129 changes: 129 additions & 0 deletions crates/cli/src/commands/blueprints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use crate::commands::apply_migration::{run_apply_migration, ApplyMigrationArgs};
use crate::flags::{GlobalFormatFlags, OutputFormat};
use crate::messenger_variant::create_emitter;
use crate::workflows::fetch_remote_workflow;
use anyhow::{bail, Result};
use clap::{Parser, Subcommand};

use marzano_messenger::emit::VisibilityLevels;
use serde::Serialize;
use serde_json::json;

#[derive(Parser, Debug, Serialize)]
pub struct Blueprints {
#[structopt(subcommand)]
pub blueprint_commands: BlueprintCommands,
}

#[derive(Subcommand, Debug, Serialize)]
pub enum BlueprintCommands {
/// List available blueprints
List(ListArgs),
/// Pull a blueprint by workflow ID
Pull(PullArgs),
/// Push a blueprint by workflow ID
Push(PushArgs),
}

#[derive(Parser, Debug, Serialize)]
pub struct ListArgs {}

async fn run_blueprint_workflow(
workflow_name: &str,
input: Option<serde_json::Value>,
parent: &GlobalFormatFlags,
) -> Result<()> {
if parent.json || parent.jsonl {
bail!("JSON output not supported for blueprints");
}

let workflow_info = fetch_remote_workflow(
&format!("https://storage.googleapis.com/grit-workflows-prod-workflow_definitions/{workflow_name}.js"),
None,
).await?;

let apply_migration_args = ApplyMigrationArgs {
input: input.map(|v| v.to_string()),
..Default::default()
};

let execution_id =
std::env::var("GRIT_EXECUTION_ID").unwrap_or_else(|_| uuid::Uuid::new_v4().to_string());

let format = OutputFormat::from(parent);
let emitter = create_emitter(
&format,
marzano_messenger::output_mode::OutputMode::default(),
None,
false,
None,
None,
VisibilityLevels::default(),
)
.await?;

run_apply_migration(
workflow_info,
vec![],
None,
apply_migration_args,
emitter,
execution_id,
)
.await?;

Ok(())
}

impl ListArgs {
pub async fn run(&self, parent: &GlobalFormatFlags) -> Result<()> {
run_blueprint_workflow("blueprints/list", None, parent).await
}
}

#[derive(Parser, Debug, Serialize)]
pub struct PullArgs {
/// The workflow ID of the blueprint to pull
#[clap(long, alias = "id")]
workflow_id: String,

/// Force pull even if the blueprint already exists
#[clap(long, short = 'f')]
force: bool,

/// File to save the blueprint to (defaults to blueprint.md)
#[clap(long, default_value = "blueprint.md")]
file: String,
}

impl PullArgs {
pub async fn run(&self, parent: &GlobalFormatFlags) -> Result<()> {
let input = json!({
"workflow_id": self.workflow_id,
"force": self.force,
"path": self.file,
});
run_blueprint_workflow("blueprints/download", Some(input), parent).await
}
}

#[derive(Parser, Debug, Serialize)]
pub struct PushArgs {
/// The workflow ID of the blueprint to push
#[clap(long, alias = "id")]
workflow_id: String,

/// File containing the blueprint (defaults to blueprint.md)
#[clap(long, default_value = "blueprint.md")]
file: String,
}

impl PushArgs {
pub async fn run(&self, parent: &GlobalFormatFlags) -> Result<()> {
let input = json!({
"workflow_id": self.workflow_id,
"path": self.file,
});
run_blueprint_workflow("blueprints/upload", Some(input), parent).await
}
}
15 changes: 15 additions & 0 deletions crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub(crate) mod lsp;

pub(crate) mod check;

pub(crate) mod blueprints;
pub(crate) mod parse;
pub(crate) mod patterns;
pub(crate) mod patterns_list;
Expand All @@ -36,6 +37,7 @@ pub(crate) mod workflows_watch;

use crate::error::GoodError;

use blueprints::{BlueprintCommands, Blueprints};
#[cfg(feature = "grit_tracing")]
use marzano_util::base64;
#[cfg(feature = "grit_tracing")]
Expand Down Expand Up @@ -142,6 +144,9 @@ pub enum Commands {
Lsp(LspArgs),
/// Print diagnostic information about the current environment
Doctor(DoctorArgs),
/// Manage blueprints for the Grit Agent
#[clap(aliases = ["blueprint", "bp"])]
Blueprints(Blueprints),
/// Authentication commands, run `grit auth --help` for more information
#[clap(name = "auth")]
Auth(Auth),
Expand Down Expand Up @@ -196,6 +201,11 @@ impl fmt::Display for Commands {
PatternCommands::Edit(_) => write!(f, "patterns edit"),
PatternCommands::Describe(_) => write!(f, "patterns describe"),
},
Commands::Blueprints(arg) => match arg.blueprint_commands {
BlueprintCommands::List(_) => write!(f, "blueprints list"),
BlueprintCommands::Pull(_) => write!(f, "blueprints pull"),
BlueprintCommands::Push(_) => write!(f, "blueprints push"),
},
#[cfg(feature = "workflows_v2")]
Commands::Workflows(arg) => match arg.workflows_commands {
WorkflowCommands::List(_) => write!(f, "workflows list"),
Expand Down Expand Up @@ -427,6 +437,11 @@ async fn run_command(_use_tracing: bool) -> Result<()> {
PatternCommands::Edit(arg) => run_patterns_edit(arg).await,
PatternCommands::Describe(arg) => run_patterns_describe(arg).await,
},
Commands::Blueprints(arg) => match arg.blueprint_commands {
BlueprintCommands::List(arg) => arg.run(&app.format_flags).await,
BlueprintCommands::Pull(arg) => arg.run(&app.format_flags).await,
BlueprintCommands::Push(arg) => arg.run(&app.format_flags).await,
},
#[cfg(feature = "workflows_v2")]
Commands::Workflows(arg) => match arg.workflows_commands {
WorkflowCommands::List(arg) => run_list_workflows(&arg, &app.format_flags).await,
Expand Down
11 changes: 9 additions & 2 deletions crates/cli/src/result_formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ use marzano_core::api::{
use marzano_core::constants::DEFAULT_FILE_NAME;
use marzano_messenger::output_mode::OutputMode;
use marzano_messenger::workflows::StatusManager;
use serde::Deserialize as _;
use std::fmt::Display;
use std::{
io::Write,
sync::{Arc, Mutex},
};

use crate::ux::{format_result_diff, indent};
use crate::ux::{format_result_diff, format_table, indent, Table};
use marzano_messenger::emit::{Messager, VisibilityLevels};

#[derive(Debug)]
Expand Down Expand Up @@ -419,7 +420,13 @@ impl Messager for FormattedMessager<'_> {
let mut writer = writer.lock().map_err(|_| anyhow!("Output lock poisoned"))?;
writeln!(writer, "[{:?}] {}", log.level, log.message)?;
} else {
let msg = format!("[{:?}] {} {:?}", log.level, log.message, log.step_id);
let msg = log
.meta
.as_ref()
.and_then(|meta| serde_json::to_value(meta).ok())
.and_then(|v| Table::deserialize(v).ok())
.map(|table| format_table(&table).to_string())
.unwrap_or_else(|| format!("[{:?}] {} {:?}", log.level, log.message, log.step_id));
match log.level {
AnalysisLogLevel::Debug => {
debug!("{}", msg);
Expand Down
128 changes: 128 additions & 0 deletions crates/cli/src/ux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,79 @@ pub fn heading(s: &str) -> String {
format!("\n{}", s.bold().yellow())
}

#[derive(Debug, serde::Deserialize)]
pub enum Format {
#[serde(rename = "table")]
Table,
}

#[derive(Debug, serde::Deserialize)]
pub struct Table {
pub format: Format,
pub headers: Option<Vec<String>>,
pub data: Vec<Vec<String>>,
}

pub fn format_table(table: &Table) -> String {
if table.data.is_empty() {
return String::new();
}

// Get max width of each column
let column_count = table.data[0].len();
let mut column_widths = vec![0; column_count];

// Account for headers in column widths
if let Some(headers) = &table.headers {
for (i, header) in headers.iter().enumerate() {
column_widths[i] = column_widths[i].max(header.len());
}
}

// Account for data in column widths
for row in &table.data {
for (i, cell) in row.iter().enumerate() {
column_widths[i] = column_widths[i].max(cell.len());
}
}

// Build formatted table string
let mut output = String::new();
// Print headers if present
if let Some(headers) = &table.headers {
let formatted_headers = headers
.iter()
.enumerate()
.map(|(i, header)| {
format!(
"{:<width$}",
header.bold().yellow(),
width = column_widths[i]
)
})
.collect::<Vec<_>>()
.join(" ");

output.push_str(&formatted_headers);
output.push('\n');
}

// Print data rows
for row in &table.data {
let formatted_row = row
.iter()
.enumerate()
.map(|(i, cell)| format!("{:<width$}", cell, width = column_widths[i]))
.collect::<Vec<_>>()
.join(" ");

output.push_str(&formatted_row);
output.push('\n');
}

output
}

#[derive(Debug)]
pub struct CheckResult<'a> {
pub pattern: &'a ResolvedGritDefinition,
Expand Down Expand Up @@ -191,3 +264,58 @@ pub fn get_check_summary(

Ok((grouped_results, summary))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_format_table() {
let table = Table {
format: Format::Table,
headers: Some(vec!["Name".to_string(), "Age".to_string()]),
data: vec![
vec!["Alice".to_string(), "25".to_string()],
vec!["Bob".to_string(), "30".to_string()],
],
};

let output = format_table(&table);
let expected = format!(
"{} {}\nAlice 25 \nBob 30 \n",
"Name ".bold().yellow(),
"Age".bold().yellow()
);

assert_eq!(output, expected);
}

#[test]
fn test_format_table_no_headers() {
let table = Table {
format: Format::Table,
headers: None,
data: vec![
vec!["Alice".to_string(), "25".to_string()],
vec!["Bob".to_string(), "30".to_string()],
],
};

let output = format_table(&table);
let expected = "Alice 25\nBob 30\n";

assert_eq!(output, expected);
}

#[test]
fn test_format_empty_table() {
let table = Table {
format: Format::Table,
headers: Some(vec!["Name".to_string(), "Age".to_string()]),
data: vec![],
};

let output = format_table(&table);
assert_eq!(output, "");
}
}
26 changes: 13 additions & 13 deletions crates/cli_bin/tests/snapshots/help__returns_expected_help.snap
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
---
source: apps/marzano/cli/tests/help.rs
source: crates/cli_bin/tests/help.rs
expression: "String::from_utf8(output.stdout)?"
---
Software maintenance on autopilot, from grit.io

Usage: grit [OPTIONS] <COMMAND>

Commands:
check Check the current directory for pattern violations
list List everything that can be applied to the current directory
apply Apply a pattern or migration to a set of files
doctor Print diagnostic information about the current environment
auth Authentication commands, run `grit auth --help` for more information
install Install supporting binaries
init Install grit modules
workflows Workflow commands, run `grit workflows --help` for more information
patterns Patterns commands, run `grit patterns --help` for more information
version Display version information about the CLI and agents
help Print this message or the help of the given subcommand(s)
check Check the current directory for pattern violations
list List everything that can be applied to the current directory
apply Apply a pattern or migration to a set of files
doctor Print diagnostic information about the current environment
blueprints Manage blueprints for the Grit Agent
auth Authentication commands, run `grit auth --help` for more information
install Install supporting binaries
init Install grit modules
workflows Workflow commands, run `grit workflows --help` for more information
patterns Patterns commands, run `grit patterns --help` for more information
version Display version information about the CLI and agents
help Print this message or the help of the given subcommand(s)

Options:
--json Enable JSON output, only supported on some commands
Expand All @@ -28,4 +29,3 @@ Options:
-V, --version Print version

For help with a specific command, run `grit help <command>`.

0 comments on commit 0d0319f

Please sign in to comment.