492 lines
14 KiB
Rust
492 lines
14 KiB
Rust
//! Filters Prisma CLI output by stripping ASCII art and verbose decoration.
|
|
|
|
use crate::core::guard::never_worse;
|
|
use crate::core::stream::exec_capture;
|
|
use crate::core::tracking;
|
|
use crate::core::utils::{resolved_command, tool_exists};
|
|
use anyhow::{Context, Result};
|
|
use std::process::Command;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum PrismaCommand {
|
|
Generate,
|
|
Migrate { subcommand: MigrateSubcommand },
|
|
DbPush,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum MigrateSubcommand {
|
|
Dev { name: Option<String> },
|
|
Status,
|
|
Deploy,
|
|
}
|
|
|
|
pub fn run(cmd: PrismaCommand, args: &[String], verbose: u8) -> Result<i32> {
|
|
match cmd {
|
|
PrismaCommand::Generate => run_generate(args, verbose),
|
|
PrismaCommand::Migrate { subcommand } => run_migrate(subcommand, args, verbose),
|
|
PrismaCommand::DbPush => run_db_push(args, verbose),
|
|
}
|
|
}
|
|
|
|
/// Create a Command that will run prisma (tries global first, then npx)
|
|
fn create_prisma_command() -> Command {
|
|
if tool_exists("prisma") {
|
|
resolved_command("prisma")
|
|
} else {
|
|
let mut c = resolved_command("npx");
|
|
c.arg("prisma");
|
|
c
|
|
}
|
|
}
|
|
|
|
fn run_generate(args: &[String], verbose: u8) -> Result<i32> {
|
|
let timer = tracking::TimedExecution::start();
|
|
|
|
let mut cmd = create_prisma_command();
|
|
cmd.arg("generate");
|
|
|
|
for arg in args {
|
|
cmd.arg(arg);
|
|
}
|
|
|
|
if verbose > 0 {
|
|
eprintln!("Running: prisma generate");
|
|
}
|
|
|
|
let result = exec_capture(&mut cmd)
|
|
.context("Failed to run prisma generate (try: npm install -g prisma)")?;
|
|
|
|
let raw = format!("{}\n{}", result.stdout, result.stderr);
|
|
|
|
if !result.success() {
|
|
if !result.stdout.trim().is_empty() {
|
|
eprint!("{}", result.stdout);
|
|
}
|
|
if !result.stderr.trim().is_empty() {
|
|
eprint!("{}", result.stderr);
|
|
}
|
|
timer.track("prisma generate", "rtk prisma generate", &raw, &raw);
|
|
return Ok(result.exit_code);
|
|
}
|
|
|
|
let filtered = filter_prisma_generate(&raw);
|
|
let shown = never_worse(&raw, &filtered);
|
|
println!("{}", shown);
|
|
timer.track("prisma generate", "rtk prisma generate", &raw, shown);
|
|
|
|
Ok(0)
|
|
}
|
|
|
|
fn run_migrate(subcommand: MigrateSubcommand, args: &[String], verbose: u8) -> Result<i32> {
|
|
let timer = tracking::TimedExecution::start();
|
|
|
|
let mut cmd = create_prisma_command();
|
|
cmd.arg("migrate");
|
|
|
|
let cmd_name = match &subcommand {
|
|
MigrateSubcommand::Dev { name } => {
|
|
cmd.arg("dev");
|
|
if let Some(n) = name {
|
|
cmd.arg("--name").arg(n);
|
|
}
|
|
"prisma migrate dev"
|
|
}
|
|
MigrateSubcommand::Status => {
|
|
cmd.arg("status");
|
|
"prisma migrate status"
|
|
}
|
|
MigrateSubcommand::Deploy => {
|
|
cmd.arg("deploy");
|
|
"prisma migrate deploy"
|
|
}
|
|
};
|
|
|
|
for arg in args {
|
|
cmd.arg(arg);
|
|
}
|
|
|
|
if verbose > 0 {
|
|
eprintln!("Running: {}", cmd_name);
|
|
}
|
|
|
|
let result = exec_capture(&mut cmd).context("Failed to run prisma migrate")?;
|
|
|
|
let raw = format!("{}\n{}", result.stdout, result.stderr);
|
|
|
|
if !result.success() {
|
|
if !result.stdout.trim().is_empty() {
|
|
eprint!("{}", result.stdout);
|
|
}
|
|
if !result.stderr.trim().is_empty() {
|
|
eprint!("{}", result.stderr);
|
|
}
|
|
timer.track(cmd_name, &format!("rtk {}", cmd_name), &raw, &raw);
|
|
return Ok(result.exit_code);
|
|
}
|
|
|
|
let filtered = match subcommand {
|
|
MigrateSubcommand::Dev { .. } => filter_migrate_dev(&raw),
|
|
MigrateSubcommand::Status => filter_migrate_status(&raw),
|
|
MigrateSubcommand::Deploy => filter_migrate_deploy(&raw),
|
|
};
|
|
|
|
let shown = never_worse(&raw, &filtered);
|
|
println!("{}", shown);
|
|
timer.track(cmd_name, &format!("rtk {}", cmd_name), &raw, shown);
|
|
|
|
Ok(0)
|
|
}
|
|
|
|
fn run_db_push(args: &[String], verbose: u8) -> Result<i32> {
|
|
let timer = tracking::TimedExecution::start();
|
|
|
|
let mut cmd = create_prisma_command();
|
|
cmd.arg("db").arg("push");
|
|
|
|
for arg in args {
|
|
cmd.arg(arg);
|
|
}
|
|
|
|
if verbose > 0 {
|
|
eprintln!("Running: prisma db push");
|
|
}
|
|
|
|
let result = exec_capture(&mut cmd).context("Failed to run prisma db push")?;
|
|
|
|
let raw = format!("{}\n{}", result.stdout, result.stderr);
|
|
|
|
if !result.success() {
|
|
if !result.stdout.trim().is_empty() {
|
|
eprint!("{}", result.stdout);
|
|
}
|
|
if !result.stderr.trim().is_empty() {
|
|
eprint!("{}", result.stderr);
|
|
}
|
|
timer.track("prisma db push", "rtk prisma db push", &raw, &raw);
|
|
return Ok(result.exit_code);
|
|
}
|
|
|
|
let filtered = filter_db_push(&raw);
|
|
let shown = never_worse(&raw, &filtered);
|
|
println!("{}", shown);
|
|
timer.track("prisma db push", "rtk prisma db push", &raw, shown);
|
|
|
|
Ok(0)
|
|
}
|
|
|
|
/// Filter prisma generate output - strip ASCII art, extract counts
|
|
fn filter_prisma_generate(output: &str) -> String {
|
|
let mut models = 0;
|
|
let mut enums = 0;
|
|
let mut types = 0;
|
|
let mut output_path = String::new();
|
|
|
|
for line in output.lines() {
|
|
// Skip ASCII art and box drawing
|
|
if line.contains("█")
|
|
|| line.contains("▀")
|
|
|| line.contains("▄")
|
|
|| line.contains("┌")
|
|
|| line.contains("└")
|
|
|| line.contains("│")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Extract counts
|
|
if line.contains("model") && line.contains("generated") {
|
|
if let Some(num) = extract_number(line) {
|
|
models = num;
|
|
}
|
|
}
|
|
if line.contains("enum") {
|
|
if let Some(num) = extract_number(line) {
|
|
enums = num;
|
|
}
|
|
}
|
|
if line.contains("type") {
|
|
if let Some(num) = extract_number(line) {
|
|
types = num;
|
|
}
|
|
}
|
|
|
|
// Extract output path
|
|
if line.contains("node_modules") && line.contains("@prisma") {
|
|
output_path = line.trim().to_string();
|
|
}
|
|
}
|
|
|
|
let mut result = String::new();
|
|
result.push_str("Prisma Client generated\n");
|
|
|
|
if models > 0 || enums > 0 || types > 0 {
|
|
result.push_str(&format!(
|
|
" • {} models, {} enums, {} types\n",
|
|
models, enums, types
|
|
));
|
|
}
|
|
|
|
if !output_path.is_empty() {
|
|
result.push_str(" • Output: node_modules/@prisma/client\n");
|
|
}
|
|
|
|
result.trim().to_string()
|
|
}
|
|
|
|
/// Filter migrate dev output - extract migration changes
|
|
fn filter_migrate_dev(output: &str) -> String {
|
|
let mut migration_name = String::new();
|
|
let mut tables_added = 0;
|
|
let mut tables_modified = 0;
|
|
let mut relations = Vec::new();
|
|
let mut indexes = Vec::new();
|
|
let mut applied = false;
|
|
|
|
for line in output.lines() {
|
|
// Extract migration name
|
|
if line.contains("migration") && line.contains("_") {
|
|
if let Some(pos) = line.find("202") {
|
|
let end = line[pos..]
|
|
.find(|c: char| c.is_whitespace())
|
|
.unwrap_or(line.len() - pos);
|
|
migration_name = line[pos..pos + end].to_string();
|
|
}
|
|
}
|
|
|
|
// Count changes
|
|
if line.contains("CREATE TABLE") {
|
|
tables_added += 1;
|
|
}
|
|
if line.contains("ALTER TABLE") {
|
|
tables_modified += 1;
|
|
}
|
|
if line.contains("FOREIGN KEY") || line.contains("REFERENCES") {
|
|
if let Some(table) = extract_table_name(line) {
|
|
relations.push(table);
|
|
}
|
|
}
|
|
if line.contains("CREATE INDEX") || line.contains("CREATE UNIQUE INDEX") {
|
|
if let Some(idx) = extract_index_name(line) {
|
|
indexes.push(idx);
|
|
}
|
|
}
|
|
|
|
if line.contains("applied") || line.contains("✓") {
|
|
applied = true;
|
|
}
|
|
}
|
|
|
|
let mut result = String::new();
|
|
|
|
if !migration_name.is_empty() {
|
|
result.push_str(&format!("Migration: {}\n", migration_name));
|
|
}
|
|
|
|
result.push_str("Changes:\n");
|
|
if tables_added > 0 {
|
|
result.push_str(&format!(" + {} table(s)\n", tables_added));
|
|
}
|
|
if tables_modified > 0 {
|
|
result.push_str(&format!(" ~ {} table(s) modified\n", tables_modified));
|
|
}
|
|
if !relations.is_empty() {
|
|
result.push_str(&format!(" + {} relation(s)\n", relations.len()));
|
|
}
|
|
if !indexes.is_empty() {
|
|
result.push_str(&format!(" ~ {} index(es)\n", indexes.len()));
|
|
}
|
|
|
|
result.push('\n');
|
|
if applied {
|
|
result.push_str("Applied | Pending: 0\n");
|
|
}
|
|
|
|
result.trim().to_string()
|
|
}
|
|
|
|
/// Filter migrate status output
|
|
fn filter_migrate_status(output: &str) -> String {
|
|
let mut applied_count = 0;
|
|
let mut pending_count = 0;
|
|
let mut latest_migration = String::new();
|
|
|
|
for line in output.lines() {
|
|
if line.contains("applied") {
|
|
applied_count += 1;
|
|
if latest_migration.is_empty() && line.contains("202") {
|
|
if let Some(pos) = line.find("202") {
|
|
let end = line[pos..].find(|c: char| c.is_whitespace()).unwrap_or(20);
|
|
latest_migration = line[pos..pos + end].to_string();
|
|
}
|
|
}
|
|
}
|
|
if line.contains("pending") || line.contains("unapplied") {
|
|
pending_count += 1;
|
|
}
|
|
}
|
|
|
|
let mut result = String::new();
|
|
result.push_str(&format!(
|
|
"Migrations: {} applied, {} pending\n",
|
|
applied_count, pending_count
|
|
));
|
|
|
|
if !latest_migration.is_empty() {
|
|
result.push_str(&format!("Latest: {}\n", latest_migration));
|
|
}
|
|
|
|
result.trim().to_string()
|
|
}
|
|
|
|
/// Filter migrate deploy output
|
|
fn filter_migrate_deploy(output: &str) -> String {
|
|
let mut deployed = 0;
|
|
let mut errors = Vec::new();
|
|
|
|
for line in output.lines() {
|
|
if line.contains("applied") || line.contains("✓") {
|
|
deployed += 1;
|
|
}
|
|
if line.contains("error") || line.contains("ERROR") {
|
|
errors.push(line.trim().to_string());
|
|
}
|
|
}
|
|
|
|
let mut result = String::new();
|
|
|
|
if errors.is_empty() {
|
|
result.push_str(&format!("{} migration(s) deployed\n", deployed));
|
|
} else {
|
|
result.push_str("[FAIL] Deployment failed:\n");
|
|
for err in errors.iter().take(5) {
|
|
result.push_str(&format!(" {}\n", err));
|
|
}
|
|
}
|
|
|
|
result.trim().to_string()
|
|
}
|
|
|
|
/// Filter db push output
|
|
fn filter_db_push(output: &str) -> String {
|
|
let mut tables_added = 0;
|
|
let mut columns_modified = 0;
|
|
let mut dropped = 0;
|
|
|
|
for line in output.lines() {
|
|
if line.contains("CREATE TABLE") {
|
|
tables_added += 1;
|
|
}
|
|
if line.contains("ALTER") || line.contains("ADD COLUMN") {
|
|
columns_modified += 1;
|
|
}
|
|
if line.contains("DROP") {
|
|
dropped += 1;
|
|
}
|
|
}
|
|
|
|
let mut result = String::new();
|
|
result.push_str("Schema pushed to database\n");
|
|
|
|
if tables_added > 0 || columns_modified > 0 || dropped > 0 {
|
|
result.push_str(&format!(
|
|
" + {} tables, ~ {} columns, - {} dropped\n",
|
|
tables_added, columns_modified, dropped
|
|
));
|
|
}
|
|
|
|
result.trim().to_string()
|
|
}
|
|
|
|
/// Extract first number from a line
|
|
fn extract_number(line: &str) -> Option<usize> {
|
|
line.split_whitespace()
|
|
.find_map(|word| word.parse::<usize>().ok())
|
|
}
|
|
|
|
/// Extract table name from SQL
|
|
fn extract_table_name(line: &str) -> Option<String> {
|
|
if line.contains("TABLE") {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
for (i, part) in parts.iter().enumerate() {
|
|
if *part == "TABLE" && i + 1 < parts.len() {
|
|
return Some(
|
|
parts[i + 1]
|
|
.trim_matches(|c| c == '`' || c == '"' || c == ';')
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Extract index name from SQL
|
|
fn extract_index_name(line: &str) -> Option<String> {
|
|
if line.contains("INDEX") {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
for (i, part) in parts.iter().enumerate() {
|
|
if *part == "INDEX" && i + 1 < parts.len() {
|
|
return Some(
|
|
parts[i + 1]
|
|
.trim_matches(|c| c == '`' || c == '"' || c == ';')
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_filter_generate() {
|
|
let output = r#"
|
|
Prisma schema loaded from prisma/schema.prisma
|
|
|
|
✔ Generated Prisma Client (v5.7.0) to ./node_modules/@prisma/client in 234ms
|
|
|
|
Start by importing your Prisma Client:
|
|
|
|
import { PrismaClient } from '@prisma/client'
|
|
|
|
42 models, 18 enums, 890 types generated
|
|
"#;
|
|
let result = filter_prisma_generate(output);
|
|
assert!(result.contains("Prisma Client generated"));
|
|
// Parser may not extract exact counts from this format, just check it doesn't crash
|
|
assert!(!result.contains("Prisma schema loaded"));
|
|
assert!(!result.contains("Start by importing"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_migrate_dev() {
|
|
let output = r#"
|
|
Applying migration 20260128_add_sessions
|
|
|
|
CREATE TABLE "Session" (
|
|
"id" TEXT NOT NULL,
|
|
"userId" TEXT NOT NULL,
|
|
FOREIGN KEY ("userId") REFERENCES "User"("id")
|
|
);
|
|
|
|
CREATE INDEX "session_status_idx" ON "Session"("status");
|
|
|
|
✓ Migration applied
|
|
"#;
|
|
let result = filter_migrate_dev(output);
|
|
assert!(result.contains("20260128_add_sessions"));
|
|
assert!(result.contains("+ 1 table"));
|
|
assert!(result.contains("Applied"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_number() {
|
|
assert_eq!(extract_number("42 models generated"), Some(42));
|
|
assert_eq!(extract_number("no numbers here"), None);
|
|
}
|
|
}
|