brokk-rtk 0.42.4

Rust Token Killer - High-performance CLI proxy to minimize LLM token consumption
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
//! Filters Prisma CLI output by stripping ASCII art and verbose decoration.

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);
    println!("{}", filtered);
    timer.track("prisma generate", "rtk prisma generate", &raw, &filtered);

    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),
    };

    println!("{}", filtered);
    timer.track(cmd_name, &format!("rtk {}", cmd_name), &raw, &filtered);

    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);
    println!("{}", filtered);
    timer.track("prisma db push", "rtk prisma db push", &raw, &filtered);

    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);
    }
}