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.
- package/README.md +18 -1
- package/dist/index.js +141 -84
- 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
|
-
|
|
61
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
612
|
+
const descriptionText = convertRichText(workItem.description, markdown);
|
|
496
613
|
if (short) {
|
|
497
|
-
|
|
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
|
|
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
|
|
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(
|