azdo-cli 0.2.0-develop.41 → 0.2.0-develop.47
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/dist/index.js +306 -301
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command9 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -26,6 +26,22 @@ var DEFAULT_FIELDS = [
|
|
|
26
26
|
"System.AreaPath",
|
|
27
27
|
"System.IterationPath"
|
|
28
28
|
];
|
|
29
|
+
function authHeaders(pat) {
|
|
30
|
+
const token = Buffer.from(`:${pat}`).toString("base64");
|
|
31
|
+
return { Authorization: `Basic ${token}` };
|
|
32
|
+
}
|
|
33
|
+
async function fetchWithErrors(url, init) {
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = await fetch(url, init);
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error("NETWORK_ERROR");
|
|
39
|
+
}
|
|
40
|
+
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
41
|
+
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
42
|
+
if (response.status === 404) throw new Error("NOT_FOUND");
|
|
43
|
+
return response;
|
|
44
|
+
}
|
|
29
45
|
function buildExtraFields(fields, requested) {
|
|
30
46
|
const result = {};
|
|
31
47
|
for (const name of requested) {
|
|
@@ -45,20 +61,7 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
45
61
|
const allFields = [...DEFAULT_FIELDS, ...extraFields];
|
|
46
62
|
url.searchParams.set("fields", allFields.join(","));
|
|
47
63
|
}
|
|
48
|
-
const
|
|
49
|
-
let response;
|
|
50
|
-
try {
|
|
51
|
-
response = await fetch(url.toString(), {
|
|
52
|
-
headers: {
|
|
53
|
-
Authorization: `Basic ${token}`
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
} catch {
|
|
57
|
-
throw new Error("NETWORK_ERROR");
|
|
58
|
-
}
|
|
59
|
-
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
60
|
-
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
61
|
-
if (response.status === 404) throw new Error("NOT_FOUND");
|
|
64
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
|
|
62
65
|
if (!response.ok) {
|
|
63
66
|
throw new Error(`HTTP_${response.status}`);
|
|
64
67
|
}
|
|
@@ -93,28 +96,32 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
93
96
|
extraFields: extraFields && extraFields.length > 0 ? buildExtraFields(data.fields, extraFields) : null
|
|
94
97
|
};
|
|
95
98
|
}
|
|
99
|
+
async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
100
|
+
const url = `https://dev.azure.com/${context.org}/${context.project}/_apis/wit/workitems/${id}?api-version=7.1&fields=${fieldName}`;
|
|
101
|
+
const response = await fetchWithErrors(url, { headers: authHeaders(pat) });
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`HTTP_${response.status}`);
|
|
104
|
+
}
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
const value = data.fields[fieldName];
|
|
107
|
+
if (value === void 0 || value === null || value === "") {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return typeof value === "object" ? JSON.stringify(value) : `${value}`;
|
|
111
|
+
}
|
|
96
112
|
async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
97
113
|
const url = new URL(
|
|
98
114
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
99
115
|
);
|
|
100
116
|
url.searchParams.set("api-version", "7.1");
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
},
|
|
110
|
-
body: JSON.stringify(operations)
|
|
111
|
-
});
|
|
112
|
-
} catch {
|
|
113
|
-
throw new Error("NETWORK_ERROR");
|
|
114
|
-
}
|
|
115
|
-
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
116
|
-
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
117
|
-
if (response.status === 404) throw new Error("NOT_FOUND");
|
|
117
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
118
|
+
method: "PATCH",
|
|
119
|
+
headers: {
|
|
120
|
+
...authHeaders(pat),
|
|
121
|
+
"Content-Type": "application/json-patch+json"
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify(operations)
|
|
124
|
+
});
|
|
118
125
|
if (response.status === 400) {
|
|
119
126
|
let serverMessage = "Unknown error";
|
|
120
127
|
try {
|
|
@@ -364,6 +371,86 @@ function unsetConfigValue(key) {
|
|
|
364
371
|
saveConfig(config);
|
|
365
372
|
}
|
|
366
373
|
|
|
374
|
+
// src/services/context.ts
|
|
375
|
+
function resolveContext(options) {
|
|
376
|
+
if (options.org && options.project) {
|
|
377
|
+
return { org: options.org, project: options.project };
|
|
378
|
+
}
|
|
379
|
+
const config = loadConfig();
|
|
380
|
+
if (config.org && config.project) {
|
|
381
|
+
return { org: config.org, project: config.project };
|
|
382
|
+
}
|
|
383
|
+
let gitContext = null;
|
|
384
|
+
try {
|
|
385
|
+
gitContext = detectAzdoContext();
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
const org = config.org || gitContext?.org;
|
|
389
|
+
const project = config.project || gitContext?.project;
|
|
390
|
+
if (org && project) {
|
|
391
|
+
return { org, project };
|
|
392
|
+
}
|
|
393
|
+
throw new Error(
|
|
394
|
+
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/services/command-helpers.ts
|
|
399
|
+
function parseWorkItemId(idStr) {
|
|
400
|
+
const id = Number.parseInt(idStr, 10);
|
|
401
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
402
|
+
process.stderr.write(
|
|
403
|
+
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
404
|
+
`
|
|
405
|
+
);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
return id;
|
|
409
|
+
}
|
|
410
|
+
function validateOrgProjectPair(options) {
|
|
411
|
+
const hasOrg = options.org !== void 0;
|
|
412
|
+
const hasProject = options.project !== void 0;
|
|
413
|
+
if (hasOrg !== hasProject) {
|
|
414
|
+
process.stderr.write(
|
|
415
|
+
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
416
|
+
);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function handleCommandError(err, id, context, scope = "write") {
|
|
421
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
422
|
+
const msg = error.message;
|
|
423
|
+
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
424
|
+
if (msg === "AUTH_FAILED") {
|
|
425
|
+
process.stderr.write(
|
|
426
|
+
`Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
|
|
427
|
+
`
|
|
428
|
+
);
|
|
429
|
+
} else if (msg === "PERMISSION_DENIED") {
|
|
430
|
+
process.stderr.write(
|
|
431
|
+
`Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
|
|
432
|
+
`
|
|
433
|
+
);
|
|
434
|
+
} else if (msg === "NOT_FOUND") {
|
|
435
|
+
process.stderr.write(
|
|
436
|
+
`Error: Work item ${id} not found in ${context?.org}/${context?.project}.
|
|
437
|
+
`
|
|
438
|
+
);
|
|
439
|
+
} else if (msg === "NETWORK_ERROR") {
|
|
440
|
+
process.stderr.write(
|
|
441
|
+
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
442
|
+
);
|
|
443
|
+
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
444
|
+
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
445
|
+
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
446
|
+
`);
|
|
447
|
+
} else {
|
|
448
|
+
process.stderr.write(`Error: ${msg}
|
|
449
|
+
`);
|
|
450
|
+
}
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
|
|
367
454
|
// src/commands/get-item.ts
|
|
368
455
|
function stripHtml(html) {
|
|
369
456
|
let text = html;
|
|
@@ -418,81 +505,18 @@ function createGetItemCommand() {
|
|
|
418
505
|
const command = new Command("get-item");
|
|
419
506
|
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(
|
|
420
507
|
async (idStr, options) => {
|
|
421
|
-
const id =
|
|
422
|
-
|
|
423
|
-
process.stderr.write(
|
|
424
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
425
|
-
`
|
|
426
|
-
);
|
|
427
|
-
process.exit(1);
|
|
428
|
-
}
|
|
429
|
-
const hasOrg = options.org !== void 0;
|
|
430
|
-
const hasProject = options.project !== void 0;
|
|
431
|
-
if (hasOrg !== hasProject) {
|
|
432
|
-
process.stderr.write(
|
|
433
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
434
|
-
);
|
|
435
|
-
process.exit(1);
|
|
436
|
-
}
|
|
508
|
+
const id = parseWorkItemId(idStr);
|
|
509
|
+
validateOrgProjectPair(options);
|
|
437
510
|
let context;
|
|
438
511
|
try {
|
|
439
|
-
|
|
440
|
-
context = { org: options.org, project: options.project };
|
|
441
|
-
} else {
|
|
442
|
-
const config = loadConfig();
|
|
443
|
-
if (config.org && config.project) {
|
|
444
|
-
context = { org: config.org, project: config.project };
|
|
445
|
-
} else {
|
|
446
|
-
let gitContext = null;
|
|
447
|
-
try {
|
|
448
|
-
gitContext = detectAzdoContext();
|
|
449
|
-
} catch {
|
|
450
|
-
}
|
|
451
|
-
const org = config.org || gitContext?.org;
|
|
452
|
-
const project = config.project || gitContext?.project;
|
|
453
|
-
if (org && project) {
|
|
454
|
-
context = { org, project };
|
|
455
|
-
} else {
|
|
456
|
-
throw new Error(
|
|
457
|
-
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
512
|
+
context = resolveContext(options);
|
|
462
513
|
const credential = await resolvePat();
|
|
463
514
|
const fieldsList = options.fields ? options.fields.split(",").map((f) => f.trim()) : loadConfig().fields;
|
|
464
515
|
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
465
516
|
const output = formatWorkItem(workItem, options.short ?? false);
|
|
466
517
|
process.stdout.write(output + "\n");
|
|
467
518
|
} catch (err) {
|
|
468
|
-
|
|
469
|
-
const msg = error.message;
|
|
470
|
-
if (msg === "AUTH_FAILED") {
|
|
471
|
-
process.stderr.write(
|
|
472
|
-
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (read)" scope.\n'
|
|
473
|
-
);
|
|
474
|
-
} else if (msg === "NOT_FOUND") {
|
|
475
|
-
process.stderr.write(
|
|
476
|
-
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
477
|
-
`
|
|
478
|
-
);
|
|
479
|
-
} else if (msg === "PERMISSION_DENIED") {
|
|
480
|
-
process.stderr.write(
|
|
481
|
-
`Error: Access denied. Your PAT may lack permissions for project "${context.project}".
|
|
482
|
-
`
|
|
483
|
-
);
|
|
484
|
-
} else if (msg === "NETWORK_ERROR") {
|
|
485
|
-
process.stderr.write(
|
|
486
|
-
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
487
|
-
);
|
|
488
|
-
} else if (msg.includes("Not in a git repository") || msg.includes("is not an Azure DevOps URL") || msg.includes("Authentication cancelled")) {
|
|
489
|
-
process.stderr.write(`Error: ${msg}
|
|
490
|
-
`);
|
|
491
|
-
} else {
|
|
492
|
-
process.stderr.write(`Error: ${msg}
|
|
493
|
-
`);
|
|
494
|
-
}
|
|
495
|
-
process.exit(1);
|
|
519
|
+
handleCommandError(err, id, context, "read");
|
|
496
520
|
}
|
|
497
521
|
}
|
|
498
522
|
);
|
|
@@ -665,48 +689,12 @@ function createConfigCommand() {
|
|
|
665
689
|
|
|
666
690
|
// src/commands/set-state.ts
|
|
667
691
|
import { Command as Command4 } from "commander";
|
|
668
|
-
function resolveContext(options) {
|
|
669
|
-
if (options.org && options.project) {
|
|
670
|
-
return { org: options.org, project: options.project };
|
|
671
|
-
}
|
|
672
|
-
const config = loadConfig();
|
|
673
|
-
if (config.org && config.project) {
|
|
674
|
-
return { org: config.org, project: config.project };
|
|
675
|
-
}
|
|
676
|
-
let gitContext = null;
|
|
677
|
-
try {
|
|
678
|
-
gitContext = detectAzdoContext();
|
|
679
|
-
} catch {
|
|
680
|
-
}
|
|
681
|
-
const org = config.org || gitContext?.org;
|
|
682
|
-
const project = config.project || gitContext?.project;
|
|
683
|
-
if (org && project) {
|
|
684
|
-
return { org, project };
|
|
685
|
-
}
|
|
686
|
-
throw new Error(
|
|
687
|
-
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
692
|
function createSetStateCommand() {
|
|
691
693
|
const command = new Command4("set-state");
|
|
692
694
|
command.description("Change the state of a work item").argument("<id>", "work item ID").argument("<state>", 'target state (e.g., "Active", "Closed")').option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
693
695
|
async (idStr, state, options) => {
|
|
694
|
-
const id =
|
|
695
|
-
|
|
696
|
-
process.stderr.write(
|
|
697
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
698
|
-
`
|
|
699
|
-
);
|
|
700
|
-
process.exit(1);
|
|
701
|
-
}
|
|
702
|
-
const hasOrg = options.org !== void 0;
|
|
703
|
-
const hasProject = options.project !== void 0;
|
|
704
|
-
if (hasOrg !== hasProject) {
|
|
705
|
-
process.stderr.write(
|
|
706
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
707
|
-
);
|
|
708
|
-
process.exit(1);
|
|
709
|
-
}
|
|
696
|
+
const id = parseWorkItemId(idStr);
|
|
697
|
+
validateOrgProjectPair(options);
|
|
710
698
|
let context;
|
|
711
699
|
try {
|
|
712
700
|
context = resolveContext(options);
|
|
@@ -730,35 +718,7 @@ function createSetStateCommand() {
|
|
|
730
718
|
`);
|
|
731
719
|
}
|
|
732
720
|
} catch (err) {
|
|
733
|
-
|
|
734
|
-
const msg = error.message;
|
|
735
|
-
if (msg === "AUTH_FAILED") {
|
|
736
|
-
process.stderr.write(
|
|
737
|
-
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (Read & Write)" scope.\n'
|
|
738
|
-
);
|
|
739
|
-
} else if (msg === "PERMISSION_DENIED") {
|
|
740
|
-
process.stderr.write(
|
|
741
|
-
`Error: Access denied. Your PAT may lack write permissions for project "${context.project}".
|
|
742
|
-
`
|
|
743
|
-
);
|
|
744
|
-
} else if (msg === "NOT_FOUND") {
|
|
745
|
-
process.stderr.write(
|
|
746
|
-
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
747
|
-
`
|
|
748
|
-
);
|
|
749
|
-
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
750
|
-
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
751
|
-
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
752
|
-
`);
|
|
753
|
-
} else if (msg === "NETWORK_ERROR") {
|
|
754
|
-
process.stderr.write(
|
|
755
|
-
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
756
|
-
);
|
|
757
|
-
} else {
|
|
758
|
-
process.stderr.write(`Error: ${msg}
|
|
759
|
-
`);
|
|
760
|
-
}
|
|
761
|
-
process.exit(1);
|
|
721
|
+
handleCommandError(err, id, context, "write");
|
|
762
722
|
}
|
|
763
723
|
}
|
|
764
724
|
);
|
|
@@ -767,40 +727,11 @@ function createSetStateCommand() {
|
|
|
767
727
|
|
|
768
728
|
// src/commands/assign.ts
|
|
769
729
|
import { Command as Command5 } from "commander";
|
|
770
|
-
function resolveContext2(options) {
|
|
771
|
-
if (options.org && options.project) {
|
|
772
|
-
return { org: options.org, project: options.project };
|
|
773
|
-
}
|
|
774
|
-
const config = loadConfig();
|
|
775
|
-
if (config.org && config.project) {
|
|
776
|
-
return { org: config.org, project: config.project };
|
|
777
|
-
}
|
|
778
|
-
let gitContext = null;
|
|
779
|
-
try {
|
|
780
|
-
gitContext = detectAzdoContext();
|
|
781
|
-
} catch {
|
|
782
|
-
}
|
|
783
|
-
const org = config.org || gitContext?.org;
|
|
784
|
-
const project = config.project || gitContext?.project;
|
|
785
|
-
if (org && project) {
|
|
786
|
-
return { org, project };
|
|
787
|
-
}
|
|
788
|
-
throw new Error(
|
|
789
|
-
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
730
|
function createAssignCommand() {
|
|
793
731
|
const command = new Command5("assign");
|
|
794
732
|
command.description("Assign a work item to a user, or unassign it").argument("<id>", "work item ID").argument("[name]", "user display name or email").option("--unassign", "clear the Assigned To field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
795
733
|
async (idStr, name, options) => {
|
|
796
|
-
const id =
|
|
797
|
-
if (!Number.isInteger(id) || id <= 0) {
|
|
798
|
-
process.stderr.write(
|
|
799
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
800
|
-
`
|
|
801
|
-
);
|
|
802
|
-
process.exit(1);
|
|
803
|
-
}
|
|
734
|
+
const id = parseWorkItemId(idStr);
|
|
804
735
|
if (!name && !options.unassign) {
|
|
805
736
|
process.stderr.write(
|
|
806
737
|
"Error: Either provide a user name or use --unassign.\n"
|
|
@@ -813,17 +744,10 @@ function createAssignCommand() {
|
|
|
813
744
|
);
|
|
814
745
|
process.exit(1);
|
|
815
746
|
}
|
|
816
|
-
|
|
817
|
-
const hasProject = options.project !== void 0;
|
|
818
|
-
if (hasOrg !== hasProject) {
|
|
819
|
-
process.stderr.write(
|
|
820
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
821
|
-
);
|
|
822
|
-
process.exit(1);
|
|
823
|
-
}
|
|
747
|
+
validateOrgProjectPair(options);
|
|
824
748
|
let context;
|
|
825
749
|
try {
|
|
826
|
-
context =
|
|
750
|
+
context = resolveContext(options);
|
|
827
751
|
const credential = await resolvePat();
|
|
828
752
|
const value = options.unassign ? "" : name;
|
|
829
753
|
const operations = [
|
|
@@ -846,35 +770,7 @@ function createAssignCommand() {
|
|
|
846
770
|
`);
|
|
847
771
|
}
|
|
848
772
|
} catch (err) {
|
|
849
|
-
|
|
850
|
-
const msg = error.message;
|
|
851
|
-
if (msg === "AUTH_FAILED") {
|
|
852
|
-
process.stderr.write(
|
|
853
|
-
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (Read & Write)" scope.\n'
|
|
854
|
-
);
|
|
855
|
-
} else if (msg === "PERMISSION_DENIED") {
|
|
856
|
-
process.stderr.write(
|
|
857
|
-
`Error: Access denied. Your PAT may lack write permissions for project "${context.project}".
|
|
858
|
-
`
|
|
859
|
-
);
|
|
860
|
-
} else if (msg === "NOT_FOUND") {
|
|
861
|
-
process.stderr.write(
|
|
862
|
-
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
863
|
-
`
|
|
864
|
-
);
|
|
865
|
-
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
866
|
-
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
867
|
-
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
868
|
-
`);
|
|
869
|
-
} else if (msg === "NETWORK_ERROR") {
|
|
870
|
-
process.stderr.write(
|
|
871
|
-
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
872
|
-
);
|
|
873
|
-
} else {
|
|
874
|
-
process.stderr.write(`Error: ${msg}
|
|
875
|
-
`);
|
|
876
|
-
}
|
|
877
|
-
process.exit(1);
|
|
773
|
+
handleCommandError(err, id, context, "write");
|
|
878
774
|
}
|
|
879
775
|
}
|
|
880
776
|
);
|
|
@@ -883,51 +779,15 @@ function createAssignCommand() {
|
|
|
883
779
|
|
|
884
780
|
// src/commands/set-field.ts
|
|
885
781
|
import { Command as Command6 } from "commander";
|
|
886
|
-
function resolveContext3(options) {
|
|
887
|
-
if (options.org && options.project) {
|
|
888
|
-
return { org: options.org, project: options.project };
|
|
889
|
-
}
|
|
890
|
-
const config = loadConfig();
|
|
891
|
-
if (config.org && config.project) {
|
|
892
|
-
return { org: config.org, project: config.project };
|
|
893
|
-
}
|
|
894
|
-
let gitContext = null;
|
|
895
|
-
try {
|
|
896
|
-
gitContext = detectAzdoContext();
|
|
897
|
-
} catch {
|
|
898
|
-
}
|
|
899
|
-
const org = config.org || gitContext?.org;
|
|
900
|
-
const project = config.project || gitContext?.project;
|
|
901
|
-
if (org && project) {
|
|
902
|
-
return { org, project };
|
|
903
|
-
}
|
|
904
|
-
throw new Error(
|
|
905
|
-
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
906
|
-
);
|
|
907
|
-
}
|
|
908
782
|
function createSetFieldCommand() {
|
|
909
783
|
const command = new Command6("set-field");
|
|
910
784
|
command.description("Set any work item field by its reference name").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Title)").argument("<value>", "new value for the field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
911
785
|
async (idStr, field, value, options) => {
|
|
912
|
-
const id =
|
|
913
|
-
|
|
914
|
-
process.stderr.write(
|
|
915
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
916
|
-
`
|
|
917
|
-
);
|
|
918
|
-
process.exit(1);
|
|
919
|
-
}
|
|
920
|
-
const hasOrg = options.org !== void 0;
|
|
921
|
-
const hasProject = options.project !== void 0;
|
|
922
|
-
if (hasOrg !== hasProject) {
|
|
923
|
-
process.stderr.write(
|
|
924
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
925
|
-
);
|
|
926
|
-
process.exit(1);
|
|
927
|
-
}
|
|
786
|
+
const id = parseWorkItemId(idStr);
|
|
787
|
+
validateOrgProjectPair(options);
|
|
928
788
|
let context;
|
|
929
789
|
try {
|
|
930
|
-
context =
|
|
790
|
+
context = resolveContext(options);
|
|
931
791
|
const credential = await resolvePat();
|
|
932
792
|
const operations = [
|
|
933
793
|
{ op: "add", path: `/fields/${field}`, value }
|
|
@@ -948,35 +808,178 @@ function createSetFieldCommand() {
|
|
|
948
808
|
`);
|
|
949
809
|
}
|
|
950
810
|
} catch (err) {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
811
|
+
handleCommandError(err, id, context, "write");
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
return command;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/commands/get-md-field.ts
|
|
819
|
+
import { Command as Command7 } from "commander";
|
|
820
|
+
|
|
821
|
+
// src/services/md-convert.ts
|
|
822
|
+
import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
823
|
+
|
|
824
|
+
// src/services/html-detect.ts
|
|
825
|
+
var HTML_TAG_REGEX = /<\/?([a-z][a-z0-9]*)\b/gi;
|
|
826
|
+
var HTML_TAGS = /* @__PURE__ */ new Set([
|
|
827
|
+
"p",
|
|
828
|
+
"br",
|
|
829
|
+
"div",
|
|
830
|
+
"span",
|
|
831
|
+
"strong",
|
|
832
|
+
"em",
|
|
833
|
+
"b",
|
|
834
|
+
"i",
|
|
835
|
+
"u",
|
|
836
|
+
"a",
|
|
837
|
+
"ul",
|
|
838
|
+
"ol",
|
|
839
|
+
"li",
|
|
840
|
+
"h1",
|
|
841
|
+
"h2",
|
|
842
|
+
"h3",
|
|
843
|
+
"h4",
|
|
844
|
+
"h5",
|
|
845
|
+
"h6",
|
|
846
|
+
"table",
|
|
847
|
+
"tr",
|
|
848
|
+
"td",
|
|
849
|
+
"th",
|
|
850
|
+
"img",
|
|
851
|
+
"pre",
|
|
852
|
+
"code"
|
|
853
|
+
]);
|
|
854
|
+
function isHtml(content) {
|
|
855
|
+
let match;
|
|
856
|
+
HTML_TAG_REGEX.lastIndex = 0;
|
|
857
|
+
while ((match = HTML_TAG_REGEX.exec(content)) !== null) {
|
|
858
|
+
if (HTML_TAGS.has(match[1].toLowerCase())) {
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/services/md-convert.ts
|
|
866
|
+
function htmlToMarkdown(html) {
|
|
867
|
+
return NodeHtmlMarkdown.translate(html);
|
|
868
|
+
}
|
|
869
|
+
function toMarkdown(content) {
|
|
870
|
+
if (isHtml(content)) {
|
|
871
|
+
return htmlToMarkdown(content);
|
|
872
|
+
}
|
|
873
|
+
return content;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/commands/get-md-field.ts
|
|
877
|
+
function createGetMdFieldCommand() {
|
|
878
|
+
const command = new Command7("get-md-field");
|
|
879
|
+
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(
|
|
880
|
+
async (idStr, field, options) => {
|
|
881
|
+
const id = parseWorkItemId(idStr);
|
|
882
|
+
validateOrgProjectPair(options);
|
|
883
|
+
let context;
|
|
884
|
+
try {
|
|
885
|
+
context = resolveContext(options);
|
|
886
|
+
const credential = await resolvePat();
|
|
887
|
+
const value = await getWorkItemFieldValue(context, id, credential.pat, field);
|
|
888
|
+
if (value === null) {
|
|
889
|
+
process.stdout.write("\n");
|
|
975
890
|
} else {
|
|
976
|
-
process.
|
|
977
|
-
`);
|
|
891
|
+
process.stdout.write(toMarkdown(value) + "\n");
|
|
978
892
|
}
|
|
979
|
-
|
|
893
|
+
} catch (err) {
|
|
894
|
+
handleCommandError(err, id, context, "read");
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
return command;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/commands/set-md-field.ts
|
|
902
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
903
|
+
import { Command as Command8 } from "commander";
|
|
904
|
+
function fail(message) {
|
|
905
|
+
process.stderr.write(`Error: ${message}
|
|
906
|
+
`);
|
|
907
|
+
process.exit(1);
|
|
908
|
+
}
|
|
909
|
+
function resolveContent(inlineContent, options) {
|
|
910
|
+
if (inlineContent && options.file) {
|
|
911
|
+
fail("Cannot specify both inline content and --file.");
|
|
912
|
+
}
|
|
913
|
+
if (options.file) {
|
|
914
|
+
return readFileContent(options.file);
|
|
915
|
+
}
|
|
916
|
+
if (inlineContent) {
|
|
917
|
+
return inlineContent;
|
|
918
|
+
}
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
function readFileContent(filePath) {
|
|
922
|
+
if (!existsSync(filePath)) {
|
|
923
|
+
fail(`File not found: ${filePath}`);
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
return readFileSync2(filePath, "utf-8");
|
|
927
|
+
} catch {
|
|
928
|
+
fail(`Cannot read file: ${filePath}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async function readStdinContent() {
|
|
932
|
+
if (process.stdin.isTTY) {
|
|
933
|
+
fail(
|
|
934
|
+
"No content provided. Pass markdown content as the third argument, use --file, or pipe via stdin."
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const chunks = [];
|
|
938
|
+
for await (const chunk of process.stdin) {
|
|
939
|
+
chunks.push(chunk);
|
|
940
|
+
}
|
|
941
|
+
const stdinContent = Buffer.concat(chunks).toString("utf-8").trimEnd();
|
|
942
|
+
if (!stdinContent) {
|
|
943
|
+
fail(
|
|
944
|
+
"No content provided via stdin. Pipe markdown content or use inline content or --file."
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
return stdinContent;
|
|
948
|
+
}
|
|
949
|
+
function formatOutput(result, options, field) {
|
|
950
|
+
if (options.json) {
|
|
951
|
+
process.stdout.write(
|
|
952
|
+
JSON.stringify({
|
|
953
|
+
id: result.id,
|
|
954
|
+
rev: result.rev,
|
|
955
|
+
field: result.fieldName,
|
|
956
|
+
value: result.fieldValue
|
|
957
|
+
}) + "\n"
|
|
958
|
+
);
|
|
959
|
+
} else {
|
|
960
|
+
process.stdout.write(`Updated work item ${result.id}: ${field} set with markdown content
|
|
961
|
+
`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function createSetMdFieldCommand() {
|
|
965
|
+
const command = new Command8("set-md-field");
|
|
966
|
+
command.description("Set a work item field with markdown content").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").argument("[content]", "markdown content to set").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").option("--file <path>", "read markdown content from file").action(
|
|
967
|
+
async (idStr, field, inlineContent, options) => {
|
|
968
|
+
const id = parseWorkItemId(idStr);
|
|
969
|
+
validateOrgProjectPair(options);
|
|
970
|
+
const content = resolveContent(inlineContent, options) ?? await readStdinContent();
|
|
971
|
+
let context;
|
|
972
|
+
try {
|
|
973
|
+
context = resolveContext(options);
|
|
974
|
+
const credential = await resolvePat();
|
|
975
|
+
const operations = [
|
|
976
|
+
{ op: "add", path: `/fields/${field}`, value: content },
|
|
977
|
+
{ op: "add", path: `/multilineFieldsFormat/${field}`, value: "Markdown" }
|
|
978
|
+
];
|
|
979
|
+
const result = await updateWorkItem(context, id, credential.pat, field, operations);
|
|
980
|
+
formatOutput(result, options, field);
|
|
981
|
+
} catch (err) {
|
|
982
|
+
handleCommandError(err, id, context, "write");
|
|
980
983
|
}
|
|
981
984
|
}
|
|
982
985
|
);
|
|
@@ -984,7 +987,7 @@ function createSetFieldCommand() {
|
|
|
984
987
|
}
|
|
985
988
|
|
|
986
989
|
// src/index.ts
|
|
987
|
-
var program = new
|
|
990
|
+
var program = new Command9();
|
|
988
991
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
989
992
|
program.addCommand(createGetItemCommand());
|
|
990
993
|
program.addCommand(createClearPatCommand());
|
|
@@ -992,6 +995,8 @@ program.addCommand(createConfigCommand());
|
|
|
992
995
|
program.addCommand(createSetStateCommand());
|
|
993
996
|
program.addCommand(createAssignCommand());
|
|
994
997
|
program.addCommand(createSetFieldCommand());
|
|
998
|
+
program.addCommand(createGetMdFieldCommand());
|
|
999
|
+
program.addCommand(createSetMdFieldCommand());
|
|
995
1000
|
program.showHelpAfterError();
|
|
996
1001
|
program.parse();
|
|
997
1002
|
if (process.argv.length <= 2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azdo-cli",
|
|
3
|
-
"version": "0.2.0-develop.
|
|
3
|
+
"version": "0.2.0-develop.47",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@napi-rs/keyring": "^1.2.0",
|
|
26
|
-
"commander": "^14.0.3"
|
|
26
|
+
"commander": "^14.0.3",
|
|
27
|
+
"node-html-markdown": "^2.0.0"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@eslint/js": "^10.0.1",
|