Skip to content

Commit 0664f29

Browse files
committed
feat: add tbdflow json output for the radar command and update README
Intent Log: - Working on the json output for the radar command - It will be used in tbdflow-ui along with info and status - Pre-sync safety snapshot
1 parent 58f6866 commit 0664f29

4 files changed

Lines changed: 173 additions & 25 deletions

File tree

README.md

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,11 @@ tbdflow init --yes --main-branch trunk
165165
tbdflow init --yes --remote git@github.com:org/repo.git
166166
```
167167

168-
| Flag | Description |
169-
|----------------|----------------------------------------------------------|
170-
| -y, --yes | Accept defaults, skip all interactive prompts. |
171-
| --main-branch | Set the main branch name (default: `main`). |
172-
| --remote | Link a remote repository URL and push the initial commit.|
168+
| Flag | Description |
169+
|---------------|-----------------------------------------------------------|
170+
| -y, --yes | Accept defaults, skip all interactive prompts. |
171+
| --main-branch | Set the main branch name (default: `main`). |
172+
| --remote | Link a remote repository URL and push the initial commit. |
173173

174174
`.tbdflow.yml`
175175
This file controls the core workflow of the tool. You can customise:
@@ -349,11 +349,11 @@ the safety net.
349349

350350
## Global options
351351

352-
| Flag | Description | Required |
353-
|-----------|----------------------------------------------------------|----------|
354-
| --verbose | Prints the underlying Git commands as they are executed. | No |
355-
| --dry-run | Simulate the command without making any changes. | No |
356-
| --json | Emit machine-readable JSON output instead of human-readable text. Supported by `info` and `status`. | No |
352+
| Flag | Description | Required |
353+
|-----------|---------------------------------------------------------------------------------------------------------------|----------|
354+
| --verbose | Prints the underlying Git commands as they are executed. | No |
355+
| --dry-run | Simulate the command without making any changes. | No |
356+
| --json | Emit machine-readable JSON output instead of human-readable text. Supported by `info`, `status`, and `radar`. | No |
357357

358358
## Commands
359359

@@ -374,19 +374,19 @@ tbdflow commit [options]
374374

375375
**Options:**
376376

377-
| Flag | Option | Description | Required |
378-
|------|------------------------|----------------------------------------------------------|----------|
379-
| -t | --type | The type of commit (e.g., feat, fix, chore). | Yes |
380-
| -s | --scope | The scope of the changes (e.g., api, ui). | No |
381-
| -m | --message | The descriptive commit message (subject line). | Yes |
382-
| | --message-file | Read the subject from a file (`-` for stdin). Conflicts with --message. | No |
383-
| | --body | Optional multi-line body for the commit message. | No |
384-
| | --body-file | Read the body from a file (`-` for stdin). Conflicts with --body. | No |
385-
| -b | --breaking | Mark the commit as a breaking change. | No |
386-
| | --breaking-description | Provide a description for the 'BREAKING CHANGE:' footer. | No |
387-
| | --tag | Optionally add and push an annotated tag to this commit. | No |
388-
| | --issue | Optionally add an issue reference to the footer. | No |
389-
| | --no-verify | Bypass the interactive DoD checklist. | No |
377+
| Flag | Option | Description | Required |
378+
|------|------------------------|-------------------------------------------------------------------------|----------|
379+
| -t | --type | The type of commit (e.g., feat, fix, chore). | Yes |
380+
| -s | --scope | The scope of the changes (e.g., api, ui). | No |
381+
| -m | --message | The descriptive commit message (subject line). | Yes |
382+
| | --message-file | Read the subject from a file (`-` for stdin). Conflicts with --message. | No |
383+
| | --body | Optional multi-line body for the commit message. | No |
384+
| | --body-file | Read the body from a file (`-` for stdin). Conflicts with --body. | No |
385+
| -b | --breaking | Mark the commit as a breaking change. | No |
386+
| | --breaking-description | Provide a description for the 'BREAKING CHANGE:' footer. | No |
387+
| | --tag | Optionally add and push an annotated tag to this commit. | No |
388+
| | --issue | Optionally add an issue reference to the footer. | No |
389+
| | --no-verify | Bypass the interactive DoD checklist. | No |
390390

391391
**Example:**
392392

@@ -951,6 +951,40 @@ trunk CI status (when `ci_check` is enabled):
951951
}
952952
```
953953

954+
**Radar data:**
955+
956+
The `radar` command returns trunk health, file churn hotspots, and overlap detection in a single payload:
957+
958+
```json
959+
{
960+
"success": true,
961+
"data": {
962+
"trunk": {
963+
"branch_name": "main",
964+
"status": "green",
965+
"last_integrated_minutes_ago": 12
966+
},
967+
"hotspots": [
968+
{ "file": "src/auth/logic.rs", "changes_count": 14 },
969+
{ "file": "src/db/schema.sql", "changes_count": 8 }
970+
],
971+
"overlaps": [
972+
{
973+
"branch": "feat/API-42-user-auth",
974+
"author": "@alicia",
975+
"commits_ahead": 2,
976+
"files": [
977+
{ "file": "src/auth/handler.rs", "level": "line" },
978+
{ "file": "src/auth/middleware.rs", "level": "file" }
979+
]
980+
}
981+
],
982+
"branches_scanned": 4,
983+
"local_files_count": 3
984+
}
985+
}
986+
```
987+
954988
#### `undo`
955989

956990
In TBD, the rule is simple: if the trunk breaks, fix it or revert it immediately. `tbdflow undo` is a smart wrapper

src/commands.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,44 @@ pub struct MonorepoStatusResponse {
142142
pub current_project: Option<String>,
143143
}
144144

145+
/// JSON payload for `tbdflow radar --json`.
146+
#[derive(Serialize)]
147+
pub struct RadarResponse {
148+
pub trunk: TrunkStatusResponse,
149+
pub hotspots: Vec<HotspotResponse>,
150+
pub overlaps: Vec<OverlapResponse>,
151+
pub branches_scanned: usize,
152+
pub local_files_count: usize,
153+
}
154+
155+
#[derive(Serialize)]
156+
pub struct TrunkStatusResponse {
157+
pub branch_name: String,
158+
pub status: String,
159+
#[serde(skip_serializing_if = "Option::is_none")]
160+
pub last_integrated_minutes_ago: Option<i64>,
161+
}
162+
163+
#[derive(Serialize)]
164+
pub struct HotspotResponse {
165+
pub file: String,
166+
pub changes_count: usize,
167+
}
168+
169+
#[derive(Serialize)]
170+
pub struct OverlapResponse {
171+
pub branch: String,
172+
pub author: String,
173+
pub commits_ahead: u32,
174+
pub files: Vec<OverlapFileResponse>,
175+
}
176+
177+
#[derive(Serialize)]
178+
pub struct OverlapFileResponse {
179+
pub file: String,
180+
pub level: String,
181+
}
182+
145183
pub fn handle_update_command() -> Result<(), anyhow::Error> {
146184
println!("{}", "--- Checking for updates ---".blue());
147185
let status = self_update::backends::github::Update::configure()

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ fn main() -> anyhow::Result<()> {
180180
commands::handle_sync(opts, &config)?;
181181
}
182182
Commands::Radar => {
183-
radar::handle_radar(opts, &config)?;
183+
radar::handle_radar(opts, &config, json)?;
184184
}
185185
Commands::Status => {
186186
commands::handle_status(opts, &config, json)?;

src/radar.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
use crate::commands::{
2+
HotspotResponse, OverlapFileResponse, OverlapResponse, RadarResponse, TbdResponse,
3+
TrunkStatusResponse,
4+
};
15
use crate::config::{Config, RadarLevel, RadarOnCommit};
26
use crate::git::RunOpts;
37
use crate::{git, intent};
@@ -247,7 +251,11 @@ fn should_ignore(file: &str, patterns: &[String]) -> bool {
247251
false
248252
}
249253

250-
pub fn handle_radar(opts: RunOpts, config: &Config) -> Result<()> {
254+
pub fn handle_radar(opts: RunOpts, config: &Config, json: bool) -> Result<()> {
255+
if json {
256+
return handle_radar_json(opts, config);
257+
}
258+
251259
println!("{}", "--- Trunk Status ---".blue());
252260
let trunk = get_trunk_status(config, opts);
253261
print_trunk_status(&trunk, &config.main_branch_name);
@@ -336,6 +344,74 @@ pub fn handle_radar(opts: RunOpts, config: &Config) -> Result<()> {
336344
Ok(())
337345
}
338346

347+
fn handle_radar_json(opts: RunOpts, config: &Config) -> Result<()> {
348+
let trunk = get_trunk_status(config, opts);
349+
350+
let trunk_status_str = match &trunk.ci {
351+
git::CiStatus::Green => "green",
352+
git::CiStatus::Failed => "failed",
353+
git::CiStatus::Pending => "pending",
354+
git::CiStatus::Unknown(_) => "unknown",
355+
};
356+
357+
let last_integrated_minutes_ago = git::get_latest_commit_time(&config.main_branch_name, opts)
358+
.ok()
359+
.flatten()
360+
.map(|dt| Utc::now().signed_duration_since(dt).num_minutes());
361+
362+
git::fetch_origin(opts)?;
363+
let hotspots = get_hotspots(config, opts)?;
364+
365+
let (overlaps, branches_scanned, local_files_count) = if config.radar.enabled {
366+
let result = scan(config, opts)?;
367+
let overlaps: Vec<OverlapResponse> = result
368+
.overlaps
369+
.iter()
370+
.map(|o| OverlapResponse {
371+
branch: o.branch_name.clone(),
372+
author: format!("@{}", o.author),
373+
commits_ahead: o.commits_ahead,
374+
files: o
375+
.overlapping_files
376+
.iter()
377+
.map(|f| OverlapFileResponse {
378+
file: f.file_path.clone(),
379+
level: match &f.overlap_kind {
380+
OverlapKind::LineOverlap { .. } => "line".to_string(),
381+
OverlapKind::SameFile => "file".to_string(),
382+
},
383+
})
384+
.collect(),
385+
})
386+
.collect();
387+
(overlaps, result.branches_scanned, result.local_files_count)
388+
} else {
389+
(vec![], 0, 0)
390+
};
391+
392+
let response = RadarResponse {
393+
trunk: TrunkStatusResponse {
394+
branch_name: config.main_branch_name.clone(),
395+
status: trunk_status_str.to_string(),
396+
last_integrated_minutes_ago,
397+
},
398+
hotspots: hotspots
399+
.into_iter()
400+
.map(|(file, changes_count)| HotspotResponse {
401+
file,
402+
changes_count,
403+
})
404+
.collect(),
405+
overlaps,
406+
branches_scanned,
407+
local_files_count,
408+
};
409+
410+
let json_output = serde_json::to_string_pretty(&TbdResponse::ok(response))?;
411+
println!("{}", json_output);
412+
Ok(())
413+
}
414+
339415
/// Silently captures a WIP snapshot if the working directory is dirty
340416
/// and the last snapshot with a hash is more than 30 minutes old.
341417
fn radar_snapshot(opts: RunOpts) {

0 commit comments

Comments
 (0)