|
1 | 1 | package commands |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bufio" |
| 5 | + "encoding/json" |
4 | 6 | "fmt" |
5 | 7 | "os" |
6 | 8 | "path/filepath" |
| 9 | + "strconv" |
7 | 10 | "strings" |
8 | 11 | "time" |
9 | 12 |
|
@@ -160,6 +163,14 @@ func registerSessionUtilityB(r *Registry) { |
160 | 163 | Category: "session", |
161 | 164 | Handler: cmdIterateInit, |
162 | 165 | }) |
| 166 | + |
| 167 | + r.Register(Command{ |
| 168 | + Name: "/auditlog", |
| 169 | + Aliases: []string{"/alog"}, |
| 170 | + Description: "show recent audit log entries [n]", |
| 171 | + Category: "session", |
| 172 | + Handler: cmdAuditLog, |
| 173 | + }) |
163 | 174 | } |
164 | 175 |
|
165 | 176 | func cmdQuit(ctx Context) Result { |
@@ -460,6 +471,90 @@ func cmdChanges(ctx Context) Result { |
460 | 471 | return Result{Handled: true} |
461 | 472 | } |
462 | 473 |
|
| 474 | +// auditLogRecord mirrors the JSON Lines format written by logAudit in features_sessions.go. |
| 475 | +type auditLogRecord struct { |
| 476 | + Timestamp string `json:"ts"` |
| 477 | + Tool string `json:"tool"` |
| 478 | + Args map[string]interface{} `json:"args,omitempty"` |
| 479 | + Result string `json:"result,omitempty"` |
| 480 | + IsError bool `json:"error,omitempty"` |
| 481 | +} |
| 482 | + |
| 483 | +func cmdAuditLog(ctx Context) Result { |
| 484 | + n := 20 |
| 485 | + if ctx.HasArg(1) { |
| 486 | + if v, err := strconv.Atoi(ctx.Arg(1)); err == nil && v > 0 { |
| 487 | + n = v |
| 488 | + } |
| 489 | + } |
| 490 | + |
| 491 | + home, _ := os.UserHomeDir() |
| 492 | + logPath := filepath.Join(home, ".iterate", "audit.jsonl") |
| 493 | + |
| 494 | + f, err := os.Open(logPath) |
| 495 | + if err != nil { |
| 496 | + if os.IsNotExist(err) { |
| 497 | + fmt.Println("No audit log found.") |
| 498 | + } else { |
| 499 | + PrintError("open audit log: %v", err) |
| 500 | + } |
| 501 | + return Result{Handled: true} |
| 502 | + } |
| 503 | + defer f.Close() |
| 504 | + |
| 505 | + // Read all lines then take the last n. |
| 506 | + var lines []string |
| 507 | + sc := bufio.NewScanner(f) |
| 508 | + sc.Buffer(make([]byte, 1024*1024), 1024*1024) |
| 509 | + for sc.Scan() { |
| 510 | + if line := strings.TrimSpace(sc.Text()); line != "" { |
| 511 | + lines = append(lines, line) |
| 512 | + } |
| 513 | + } |
| 514 | + |
| 515 | + start := len(lines) - n |
| 516 | + if start < 0 { |
| 517 | + start = 0 |
| 518 | + } |
| 519 | + recent := lines[start:] |
| 520 | + |
| 521 | + fmt.Printf("%s── Audit log (last %d) ─────────────%s\n", ColorDim, len(recent), ColorReset) |
| 522 | + for _, line := range recent { |
| 523 | + var rec auditLogRecord |
| 524 | + if err := json.Unmarshal([]byte(line), &rec); err != nil { |
| 525 | + fmt.Printf(" %s%s%s\n", ColorDim, line, ColorReset) |
| 526 | + continue |
| 527 | + } |
| 528 | + ts := rec.Timestamp |
| 529 | + if t, err := time.Parse(time.RFC3339, ts); err == nil { |
| 530 | + ts = t.Local().Format("15:04:05") |
| 531 | + } |
| 532 | + statusCol := ColorLime |
| 533 | + statusIcon := "✓" |
| 534 | + if rec.IsError { |
| 535 | + statusCol = ColorRed |
| 536 | + statusIcon = "✗" |
| 537 | + } |
| 538 | + result := rec.Result |
| 539 | + if len(result) > 60 { |
| 540 | + result = result[:60] + "…" |
| 541 | + } |
| 542 | + result = strings.ReplaceAll(result, "\n", " ") |
| 543 | + fmt.Printf(" %s%s%s %s%-18s%s %s%s%s\n", |
| 544 | + statusCol, statusIcon, ColorReset, |
| 545 | + ColorDim, ts, ColorReset, |
| 546 | + ColorBold, rec.Tool, ColorReset) |
| 547 | + if result != "" { |
| 548 | + fmt.Printf(" %s%s%s\n", ColorDim, result, ColorReset) |
| 549 | + } |
| 550 | + } |
| 551 | + if len(recent) == 0 { |
| 552 | + fmt.Println(" (empty)") |
| 553 | + } |
| 554 | + fmt.Printf("%s──────────────────────────────────%s\n\n", ColorDim, ColorReset) |
| 555 | + return Result{Handled: true} |
| 556 | +} |
| 557 | + |
463 | 558 | func cmdIterateInit(ctx Context) Result { |
464 | 559 | iterateMDPath := filepath.Join(ctx.RepoPath, "ITERATE.md") |
465 | 560 | if _, err := os.Stat(iterateMDPath); err == nil { |
|
0 commit comments