diff --git a/DevOps/Actions/ActionHelpers.cs b/DevOps/Actions/ActionHelpers.cs
index 61521b4..395def5 100644
--- a/DevOps/Actions/ActionHelpers.cs
+++ b/DevOps/Actions/ActionHelpers.cs
@@ -1,11 +1,66 @@
using DevOps.Responses;
using DevOps.Services;
+using DevOps.Utils;
+using Newtonsoft.Json;
using Spectre.Console;
namespace DevOps.Actions;
internal static class ActionHelpers
{
+ ///
+ /// Writes work items as machine-readable JSON or CSV to stdout. Returns a non-zero
+ /// exit code (with an error) for an unknown format.
+ ///
+ internal static int WriteWorkItemsOutput(List items, string format)
+ {
+ switch (format?.ToLowerInvariant())
+ {
+ case "json":
+ var projected = items.Select(i => new
+ {
+ id = i.Id,
+ type = i.Fields.WorkItemType,
+ title = i.Fields.Title,
+ state = i.Fields.State,
+ assignedTo = i.Fields.AssignedTo?.DisplayName,
+ project = i.Fields.TeamProject,
+ parentId = i.Fields.ParentId,
+ priority = i.Fields.Priority,
+ createdDate = i.Fields.CreatedDate,
+ changedDate = i.Fields.ChangedDate
+ });
+ Console.WriteLine(JsonConvert.SerializeObject(projected, Formatting.Indented));
+ return 0;
+
+ case "csv":
+ Console.WriteLine("id,type,title,state,assigned_to,project,parent_id,priority,created,changed");
+ foreach (var i in items)
+ {
+ var f = i.Fields;
+ Console.WriteLine(string.Join(",",
+ i.Id,
+ Csv(f.WorkItemType), Csv(f.Title), Csv(f.State),
+ Csv(f.AssignedTo?.DisplayName), Csv(f.TeamProject),
+ f.ParentId?.ToString() ?? "", f.Priority?.ToString() ?? "",
+ f.CreatedDate.ToString("yyyy-MM-dd"), f.ChangedDate.ToString("yyyy-MM-dd")));
+ }
+ return 0;
+
+ default:
+ ConsoleHelper.WriteError($"Unknown output format '{format}'. Use 'json' or 'csv'.");
+ return 1;
+ }
+ }
+
+ private static string Csv(string value)
+ {
+ if (string.IsNullOrEmpty(value)) return "";
+ return value.Contains(',') || value.Contains('"') || value.Contains('\n')
+ ? "\"" + value.Replace("\"", "\"\"") + "\""
+ : value;
+ }
+
/// Creates a table with bold headers, sized to the terminal, using the configured border.
internal static Table NewTable(params string[] columns)
{
diff --git a/DevOps/Actions/GetAction.cs b/DevOps/Actions/GetAction.cs
index 2acf23c..f96988c 100644
--- a/DevOps/Actions/GetAction.cs
+++ b/DevOps/Actions/GetAction.cs
@@ -13,6 +13,9 @@ internal static async Task Execute(GetOptions opts, CancellationToken ct)
var project = ConfigService.ResolveProject(opts.Project);
var item = await HttpService.GetWorkItem(opts.Id, project, ct);
+ if (!string.IsNullOrEmpty(opts.Output))
+ return ActionHelpers.WriteWorkItemsOutput([item], opts.Output);
+
Console.WriteLine($"ID : {item.Id}");
Console.WriteLine($"Type : {item.Fields.WorkItemType}");
Console.WriteLine($"Title : {item.Fields.Title}");
diff --git a/DevOps/Actions/ListAction.cs b/DevOps/Actions/ListAction.cs
index aedaac6..86f814c 100644
--- a/DevOps/Actions/ListAction.cs
+++ b/DevOps/Actions/ListAction.cs
@@ -14,6 +14,9 @@ internal static async Task Execute(ListOptions opts, CancellationToken ct)
var project = ConfigService.ResolveProject(opts.Project);
var (items, totalMatched) = await HttpService.ListWorkItems(project, opts.State, opts.Type, opts.AssignedTo, opts.Query, opts.ParentId, opts.Top, ct);
+ if (!string.IsNullOrEmpty(opts.Output))
+ return ActionHelpers.WriteWorkItemsOutput(items, opts.Output);
+
if (items.Count == 0)
{
Console.WriteLine("No work items found.");
diff --git a/DevOps/Actions/MineAction.cs b/DevOps/Actions/MineAction.cs
index 5ad3719..e0c7ebd 100644
--- a/DevOps/Actions/MineAction.cs
+++ b/DevOps/Actions/MineAction.cs
@@ -14,6 +14,9 @@ internal static async Task Execute(MineOptions opts, CancellationToken ct)
var project = ConfigService.ResolveProject(opts.Project);
var (items, totalMatched) = await HttpService.ListWorkItems(project, opts.State, opts.Type, "me", opts.Query, opts.ParentId, opts.Top, ct);
+ if (!string.IsNullOrEmpty(opts.Output))
+ return ActionHelpers.WriteWorkItemsOutput(items, opts.Output);
+
if (items.Count == 0)
{
Console.WriteLine("No work items assigned to you.");
diff --git a/DevOps/Options/GetOptions.cs b/DevOps/Options/GetOptions.cs
index 1ead6ec..7e4b2d1 100644
--- a/DevOps/Options/GetOptions.cs
+++ b/DevOps/Options/GetOptions.cs
@@ -10,4 +10,7 @@ public class GetOptions
[Option('p', "project", Required = false, HelpText = "Project name. Uses default if configured.")]
public string Project { get; set; }
+
+ [Option('o', "output", Required = false, HelpText = "Output format: 'json' or 'csv'. Defaults to a detailed view.")]
+ public string Output { get; set; }
}
diff --git a/DevOps/Options/ListOptions.cs b/DevOps/Options/ListOptions.cs
index 8ad56e3..63e9772 100644
--- a/DevOps/Options/ListOptions.cs
+++ b/DevOps/Options/ListOptions.cs
@@ -28,4 +28,7 @@ public class ListOptions
[Option('n', "top", Required = false, Default = 50, HelpText = "Maximum number of work items to fetch (default: 50).")]
public int Top { get; set; }
+
+ [Option('o', "output", Required = false, HelpText = "Output format: 'json' or 'csv'. Defaults to a table.")]
+ public string Output { get; set; }
}
diff --git a/DevOps/Options/MineOptions.cs b/DevOps/Options/MineOptions.cs
index d68aa06..14939cc 100644
--- a/DevOps/Options/MineOptions.cs
+++ b/DevOps/Options/MineOptions.cs
@@ -22,4 +22,7 @@ public class MineOptions
[Option('n', "top", Required = false, Default = 50, HelpText = "Maximum number of work items to fetch (default: 50).")]
public int Top { get; set; }
+
+ [Option('o', "output", Required = false, HelpText = "Output format: 'json' or 'csv'. Defaults to a table.")]
+ public string Output { get; set; }
}
diff --git a/README.md b/README.md
index ac52b66..ab8b5f0 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,7 @@ devops get -i 1234 -p AnotherProject
|---|---|---|
| `--id` | `-i` | Work item ID (required) |
| `--project` | `-p` | Project name (uses default if configured) |
+| `--output` | `-o` | Output format: `json` or `csv`. Defaults to a detailed view |
---
@@ -112,6 +113,7 @@ devops mine -p 1234 # only children of work item 1234
| `--query` | `-q` | Additional WIQL WHERE clause |
| `--parent` | `-p` | Filter by parent work item ID |
| `--top` | `-n` | Maximum number of work items to fetch (default: 50) |
+| `--output` | `-o` | Output format: `json` or `csv`. Defaults to a table |
---
@@ -124,6 +126,7 @@ devops list -t Bug -a me
devops list -P MyProject -s "In Progress" -t Task
devops list -p 1234 # only children of work item 1234
devops list -n 200 # fetch up to 200 items instead of the default 50
+devops list -s Active -o json # machine-readable output for scripting
devops list -q "[System.IterationPath] UNDER 'MyProject\\Sprint 1'"
```
@@ -138,6 +141,7 @@ Queries fetch up to `--top` items (default 50). When more match than were fetche
| `--query` | `-q` | WIQL WHERE clause for advanced filtering |
| `--parent` | `-p` | Filter by parent work item ID |
| `--top` | `-n` | Maximum number of work items to fetch (default: 50) |
+| `--output` | `-o` | Output format: `json` or `csv`. Defaults to a table |
---