azdo-cli 0.2.0-develop.62 → 0.2.0-develop.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -1
  2. package/dist/index.js +141 -84
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -53,7 +53,7 @@ azdo set-state 12345 "Active"
53
53
 
54
54
  | Command | Purpose | Common Flags |
55
55
  | --- | --- | --- |
56
- | `azdo get-item <id>` | Read a work item | `--short`, `--fields`, `--org`, `--project` |
56
+ | `azdo get-item <id>` | Read a work item | `--short`, `--fields`, `--markdown`, `--org`, `--project` |
57
57
  | `azdo set-state <id> <state>` | Change work item state | `--json`, `--org`, `--project` |
58
58
  | `azdo assign <id> [name]` | Assign or unassign owner | `--unassign`, `--json`, `--org`, `--project` |
59
59
  | `azdo set-field <id> <field> <value>` | Update any field | `--json`, `--org`, `--project` |
@@ -75,6 +75,12 @@ azdo get-item 12345 --short
75
75
 
76
76
  # Include extra fields for this call
77
77
  azdo get-item 12345 --fields "System.Tags,Microsoft.VSTS.Common.Priority"
78
+
79
+ # Convert rich text fields to markdown
80
+ azdo get-item 12345 --markdown
81
+
82
+ # Disable markdown even if config is on
83
+ azdo get-item 12345 --no-markdown
78
84
  ```
79
85
 
80
86
  ```bash
@@ -89,6 +95,14 @@ azdo assign 12345 --unassign
89
95
  azdo set-field 12345 System.Title "Updated title"
90
96
  ```
91
97
 
98
+ ### Markdown Display
99
+
100
+ The `get-item` command can convert HTML rich-text fields to readable markdown. Resolution order:
101
+
102
+ 1. `--markdown` / `--no-markdown` flag (highest priority)
103
+ 2. Config setting: `azdo config set markdown true`
104
+ 3. Default: off (HTML stripped to plain text)
105
+
92
106
  ### Markdown Field Commands
93
107
 
94
108
  ```bash
@@ -114,6 +128,9 @@ azdo config list
114
128
  # Interactive setup
115
129
  azdo config wizard
116
130
 
131
+ # Enable markdown display for all get-item calls
132
+ azdo config set markdown true
133
+
117
134
  # Set/get/unset values
118
135
  azdo config set fields "System.Tags,Custom.Priority"
119
136
  azdo config get fields
package/dist/index.js CHANGED
@@ -42,6 +42,19 @@ async function fetchWithErrors(url, init) {
42
42
  if (response.status === 404) throw new Error("NOT_FOUND");
43
43
  return response;
44
44
  }
45
+ async function readResponseMessage(response) {
46
+ try {
47
+ const body = await response.json();
48
+ if (typeof body.message === "string" && body.message.trim() !== "") {
49
+ return body.message.trim();
50
+ }
51
+ } catch {
52
+ }
53
+ return null;
54
+ }
55
+ function normalizeFieldList(fields) {
56
+ return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
57
+ }
45
58
  function buildExtraFields(fields, requested) {
46
59
  const result = {};
47
60
  for (const name of requested) {
@@ -57,11 +70,18 @@ async function getWorkItem(context, id, pat, extraFields) {
57
70
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
58
71
  );
59
72
  url.searchParams.set("api-version", "7.1");
60
- if (extraFields && extraFields.length > 0) {
61
- const allFields = [...DEFAULT_FIELDS, ...extraFields];
73
+ const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
74
+ if (normalizedExtraFields.length > 0) {
75
+ const allFields = normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields]);
62
76
  url.searchParams.set("fields", allFields.join(","));
63
77
  }
64
78
  const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
79
+ if (response.status === 400) {
80
+ const serverMessage = await readResponseMessage(response);
81
+ if (serverMessage) {
82
+ throw new Error(`BAD_REQUEST: ${serverMessage}`);
83
+ }
84
+ }
65
85
  if (!response.ok) {
66
86
  throw new Error(`HTTP_${response.status}`);
67
87
  }
@@ -93,7 +113,7 @@ async function getWorkItem(context, id, pat, extraFields) {
93
113
  areaPath: data.fields["System.AreaPath"],
94
114
  iterationPath: data.fields["System.IterationPath"],
95
115
  url: data._links.html.href,
96
- extraFields: extraFields && extraFields.length > 0 ? buildExtraFields(data.fields, extraFields) : null
116
+ extraFields: normalizedExtraFields.length > 0 ? buildExtraFields(data.fields, normalizedExtraFields) : null
97
117
  };
98
118
  }
99
119
  async function getWorkItemFieldValue(context, id, pat, fieldName) {
@@ -103,6 +123,12 @@ async function getWorkItemFieldValue(context, id, pat, fieldName) {
103
123
  url.searchParams.set("api-version", "7.1");
104
124
  url.searchParams.set("fields", fieldName);
105
125
  const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
126
+ if (response.status === 400) {
127
+ const serverMessage = await readResponseMessage(response);
128
+ if (serverMessage) {
129
+ throw new Error(`BAD_REQUEST: ${serverMessage}`);
130
+ }
131
+ }
106
132
  if (!response.ok) {
107
133
  throw new Error(`HTTP_${response.status}`);
108
134
  }
@@ -127,12 +153,7 @@ async function updateWorkItem(context, id, pat, fieldName, operations) {
127
153
  body: JSON.stringify(operations)
128
154
  });
129
155
  if (response.status === 400) {
130
- let serverMessage = "Unknown error";
131
- try {
132
- const body = await response.json();
133
- if (body.message) serverMessage = body.message;
134
- } catch {
135
- }
156
+ const serverMessage = await readResponseMessage(response) ?? "Unknown error";
136
157
  throw new Error(`UPDATE_REJECTED: ${serverMessage}`);
137
158
  }
138
159
  if (!response.ok) {
@@ -315,6 +336,13 @@ var SETTINGS = [
315
336
  type: "string[]",
316
337
  example: "System.Tags,Custom.Priority",
317
338
  required: false
339
+ },
340
+ {
341
+ key: "markdown",
342
+ description: "Convert rich text fields to markdown on display",
343
+ type: "boolean",
344
+ example: "true",
345
+ required: false
318
346
  }
319
347
  ];
320
348
  var VALID_KEYS = SETTINGS.map((s) => s.key);
@@ -348,7 +376,7 @@ function saveConfig(config) {
348
376
  }
349
377
  function validateKey(key) {
350
378
  if (!VALID_KEYS.includes(key)) {
351
- throw new Error(`Unknown setting key "${key}". Valid keys: org, project, fields`);
379
+ throw new Error(`Unknown setting key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
352
380
  }
353
381
  }
354
382
  function getConfigValue(key) {
@@ -361,6 +389,11 @@ function setConfigValue(key, value) {
361
389
  const config = loadConfig();
362
390
  if (value === "") {
363
391
  delete config[key];
392
+ } else if (key === "markdown") {
393
+ if (value !== "true" && value !== "false") {
394
+ throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
395
+ }
396
+ config.markdown = value === "true";
364
397
  } else if (key === "fields") {
365
398
  config.fields = value.split(",").map((s) => s.trim());
366
399
  } else {
@@ -399,6 +432,61 @@ function resolveContext(options) {
399
432
  );
400
433
  }
401
434
 
435
+ // src/services/md-convert.ts
436
+ import { NodeHtmlMarkdown } from "node-html-markdown";
437
+
438
+ // src/services/html-detect.ts
439
+ var HTML_TAG_REGEX = /<\/?([a-z][a-z0-9]*)\b/gi;
440
+ var HTML_TAGS = /* @__PURE__ */ new Set([
441
+ "p",
442
+ "br",
443
+ "div",
444
+ "span",
445
+ "strong",
446
+ "em",
447
+ "b",
448
+ "i",
449
+ "u",
450
+ "a",
451
+ "ul",
452
+ "ol",
453
+ "li",
454
+ "h1",
455
+ "h2",
456
+ "h3",
457
+ "h4",
458
+ "h5",
459
+ "h6",
460
+ "table",
461
+ "tr",
462
+ "td",
463
+ "th",
464
+ "img",
465
+ "pre",
466
+ "code"
467
+ ]);
468
+ function isHtml(content) {
469
+ let match;
470
+ HTML_TAG_REGEX.lastIndex = 0;
471
+ while ((match = HTML_TAG_REGEX.exec(content)) !== null) {
472
+ if (HTML_TAGS.has(match[1].toLowerCase())) {
473
+ return true;
474
+ }
475
+ }
476
+ return false;
477
+ }
478
+
479
+ // src/services/md-convert.ts
480
+ function htmlToMarkdown(html) {
481
+ return NodeHtmlMarkdown.translate(html);
482
+ }
483
+ function toMarkdown(content) {
484
+ if (isHtml(content)) {
485
+ return htmlToMarkdown(content);
486
+ }
487
+ return content;
488
+ }
489
+
402
490
  // src/services/command-helpers.ts
403
491
  function parseWorkItemId(idStr) {
404
492
  const id = Number.parseInt(idStr, 10);
@@ -421,7 +509,7 @@ function validateOrgProjectPair(options) {
421
509
  process.exit(1);
422
510
  }
423
511
  }
424
- function handleCommandError(err, id, context, scope = "write") {
512
+ function handleCommandError(err, id, context, scope = "write", exit = true) {
425
513
  const error = err instanceof Error ? err : new Error(String(err));
426
514
  const msg = error.message;
427
515
  const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
@@ -444,6 +532,10 @@ function handleCommandError(err, id, context, scope = "write") {
444
532
  process.stderr.write(
445
533
  "Error: Could not connect to Azure DevOps. Check your network connection.\n"
446
534
  );
535
+ } else if (msg.startsWith("BAD_REQUEST:")) {
536
+ const serverMsg = msg.replace("BAD_REQUEST: ", "");
537
+ process.stderr.write(`Error: Request rejected: ${serverMsg}
538
+ `);
447
539
  } else if (msg.startsWith("UPDATE_REJECTED:")) {
448
540
  const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
449
541
  process.stderr.write(`Error: Update rejected: ${serverMsg}
@@ -452,10 +544,21 @@ function handleCommandError(err, id, context, scope = "write") {
452
544
  process.stderr.write(`Error: ${msg}
453
545
  `);
454
546
  }
455
- process.exit(1);
547
+ if (exit) {
548
+ process.exit(1);
549
+ } else {
550
+ process.exitCode = 1;
551
+ }
456
552
  }
457
553
 
458
554
  // src/commands/get-item.ts
555
+ function parseRequestedFields(raw) {
556
+ if (raw === void 0) return void 0;
557
+ const source = Array.isArray(raw) ? raw : [raw];
558
+ const tokens = source.flatMap((entry) => entry.split(/[,\s]+/)).map((field) => field.trim()).filter((field) => field.length > 0);
559
+ if (tokens.length === 0) return void 0;
560
+ return Array.from(new Set(tokens));
561
+ }
459
562
  function stripHtml(html) {
460
563
  let text = html;
461
564
  text = text.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
@@ -472,7 +575,24 @@ function stripHtml(html) {
472
575
  text = text.replace(/\n{3,}/g, "\n\n");
473
576
  return text.trim();
474
577
  }
475
- function formatWorkItem(workItem, short) {
578
+ function convertRichText(html, markdown) {
579
+ if (!html) return "";
580
+ return markdown ? toMarkdown(html) : stripHtml(html);
581
+ }
582
+ function formatExtraFields(extraFields, markdown) {
583
+ return Object.entries(extraFields).map(([refName, value]) => {
584
+ const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
585
+ const displayValue = markdown ? toMarkdown(value) : value;
586
+ return `${fieldLabel.padEnd(13)}${displayValue}`;
587
+ });
588
+ }
589
+ function summarizeDescription(text, label) {
590
+ const descLines = text.split("\n").filter((l) => l.trim() !== "");
591
+ const firstThree = descLines.slice(0, 3);
592
+ const suffix = descLines.length > 3 ? "\n..." : "";
593
+ return [`${label("Description:")}${firstThree.join("\n")}${suffix}`];
594
+ }
595
+ function formatWorkItem(workItem, short, markdown = false) {
476
596
  const lines = [];
477
597
  const label = (name) => name.padEnd(13);
478
598
  lines.push(`${label("ID:")}${workItem.id}`);
@@ -486,19 +606,12 @@ function formatWorkItem(workItem, short) {
486
606
  }
487
607
  lines.push(`${label("URL:")}${workItem.url}`);
488
608
  if (workItem.extraFields) {
489
- for (const [refName, value] of Object.entries(workItem.extraFields)) {
490
- const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
491
- lines.push(`${fieldLabel.padEnd(13)}${value}`);
492
- }
609
+ lines.push(...formatExtraFields(workItem.extraFields, markdown));
493
610
  }
494
611
  lines.push("");
495
- const descriptionText = workItem.description ? stripHtml(workItem.description) : "";
612
+ const descriptionText = convertRichText(workItem.description, markdown);
496
613
  if (short) {
497
- const descLines = descriptionText.split("\n").filter((l) => l.trim() !== "");
498
- const firstThree = descLines.slice(0, 3);
499
- const truncated = descLines.length > 3;
500
- const descSummary = firstThree.join("\n") + (truncated ? "\n..." : "");
501
- lines.push(`${label("Description:")}${descSummary}`);
614
+ lines.push(...summarizeDescription(descriptionText, label));
502
615
  } else {
503
616
  lines.push("Description:");
504
617
  lines.push(descriptionText);
@@ -507,7 +620,7 @@ function formatWorkItem(workItem, short) {
507
620
  }
508
621
  function createGetItemCommand() {
509
622
  const command = new Command("get-item");
510
- command.description("Retrieve an Azure DevOps work item by ID").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--short", "show abbreviated output").option("--fields <fields>", "comma-separated additional field reference names").action(
623
+ command.description("Retrieve an Azure DevOps work item by ID").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--short", "show abbreviated output").option("--fields <fields>", "comma-separated additional field reference names").option("--markdown", "convert rich text fields to markdown").action(
511
624
  async (idStr, options) => {
512
625
  const id = parseWorkItemId(idStr);
513
626
  validateOrgProjectPair(options);
@@ -515,12 +628,13 @@ function createGetItemCommand() {
515
628
  try {
516
629
  context = resolveContext(options);
517
630
  const credential = await resolvePat();
518
- const fieldsList = options.fields ? options.fields.split(",").map((f) => f.trim()) : loadConfig().fields;
631
+ const fieldsList = options.fields !== void 0 ? parseRequestedFields(options.fields) : parseRequestedFields(loadConfig().fields);
519
632
  const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
520
- const output = formatWorkItem(workItem, options.short ?? false);
633
+ const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
634
+ const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
521
635
  process.stdout.write(output + "\n");
522
636
  } catch (err) {
523
- handleCommandError(err, id, context, "read");
637
+ handleCommandError(err, id, context, "read", false);
524
638
  }
525
639
  }
526
640
  );
@@ -821,63 +935,6 @@ function createSetFieldCommand() {
821
935
 
822
936
  // src/commands/get-md-field.ts
823
937
  import { Command as Command7 } from "commander";
824
-
825
- // src/services/md-convert.ts
826
- import { NodeHtmlMarkdown } from "node-html-markdown";
827
-
828
- // src/services/html-detect.ts
829
- var HTML_TAG_REGEX = /<\/?([a-z][a-z0-9]*)\b/gi;
830
- var HTML_TAGS = /* @__PURE__ */ new Set([
831
- "p",
832
- "br",
833
- "div",
834
- "span",
835
- "strong",
836
- "em",
837
- "b",
838
- "i",
839
- "u",
840
- "a",
841
- "ul",
842
- "ol",
843
- "li",
844
- "h1",
845
- "h2",
846
- "h3",
847
- "h4",
848
- "h5",
849
- "h6",
850
- "table",
851
- "tr",
852
- "td",
853
- "th",
854
- "img",
855
- "pre",
856
- "code"
857
- ]);
858
- function isHtml(content) {
859
- let match;
860
- HTML_TAG_REGEX.lastIndex = 0;
861
- while ((match = HTML_TAG_REGEX.exec(content)) !== null) {
862
- if (HTML_TAGS.has(match[1].toLowerCase())) {
863
- return true;
864
- }
865
- }
866
- return false;
867
- }
868
-
869
- // src/services/md-convert.ts
870
- function htmlToMarkdown(html) {
871
- return NodeHtmlMarkdown.translate(html);
872
- }
873
- function toMarkdown(content) {
874
- if (isHtml(content)) {
875
- return htmlToMarkdown(content);
876
- }
877
- return content;
878
- }
879
-
880
- // src/commands/get-md-field.ts
881
938
  function createGetMdFieldCommand() {
882
939
  const command = new Command7("get-md-field");
883
940
  command.description("Get a work item field value, converting HTML to markdown").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.2.0-develop.62",
3
+ "version": "0.2.0-develop.71",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {