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 | ---