azdo-cli 0.2.0-develop.24 → 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 +324 -306
- 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) {
|
|
@@ -37,25 +53,15 @@ function buildExtraFields(fields, requested) {
|
|
|
37
53
|
return Object.keys(result).length > 0 ? result : null;
|
|
38
54
|
}
|
|
39
55
|
async function getWorkItem(context, id, pat, extraFields) {
|
|
40
|
-
|
|
56
|
+
const url = new URL(
|
|
57
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
58
|
+
);
|
|
59
|
+
url.searchParams.set("api-version", "7.1");
|
|
41
60
|
if (extraFields && extraFields.length > 0) {
|
|
42
61
|
const allFields = [...DEFAULT_FIELDS, ...extraFields];
|
|
43
|
-
url
|
|
44
|
-
}
|
|
45
|
-
const token = Buffer.from(`:${pat}`).toString("base64");
|
|
46
|
-
let response;
|
|
47
|
-
try {
|
|
48
|
-
response = await fetch(url, {
|
|
49
|
-
headers: {
|
|
50
|
-
Authorization: `Basic ${token}`
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
} catch {
|
|
54
|
-
throw new Error("NETWORK_ERROR");
|
|
62
|
+
url.searchParams.set("fields", allFields.join(","));
|
|
55
63
|
}
|
|
56
|
-
|
|
57
|
-
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
58
|
-
if (response.status === 404) throw new Error("NOT_FOUND");
|
|
64
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
|
|
59
65
|
if (!response.ok) {
|
|
60
66
|
throw new Error(`HTTP_${response.status}`);
|
|
61
67
|
}
|
|
@@ -90,25 +96,32 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
90
96
|
extraFields: extraFields && extraFields.length > 0 ? buildExtraFields(data.fields, extraFields) : null
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
|
-
async function
|
|
94
|
-
const url = `https://dev.azure.com/${context.org}/${context.project}/_apis/wit/workitems/${id}?api-version=7.1`;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
response = await fetch(url, {
|
|
99
|
-
method: "PATCH",
|
|
100
|
-
headers: {
|
|
101
|
-
Authorization: `Basic ${token}`,
|
|
102
|
-
"Content-Type": "application/json-patch+json"
|
|
103
|
-
},
|
|
104
|
-
body: JSON.stringify(operations)
|
|
105
|
-
});
|
|
106
|
-
} catch {
|
|
107
|
-
throw new Error("NETWORK_ERROR");
|
|
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}`);
|
|
108
104
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
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
|
+
}
|
|
112
|
+
async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
113
|
+
const url = new URL(
|
|
114
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
115
|
+
);
|
|
116
|
+
url.searchParams.set("api-version", "7.1");
|
|
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
|
+
});
|
|
112
125
|
if (response.status === 400) {
|
|
113
126
|
let serverMessage = "Unknown error";
|
|
114
127
|
try {
|
|
@@ -166,6 +179,10 @@ async function deletePat() {
|
|
|
166
179
|
}
|
|
167
180
|
|
|
168
181
|
// src/services/auth.ts
|
|
182
|
+
function normalizePat(rawPat) {
|
|
183
|
+
const trimmedPat = rawPat.trim();
|
|
184
|
+
return trimmedPat.length > 0 ? trimmedPat : null;
|
|
185
|
+
}
|
|
169
186
|
async function promptForPat() {
|
|
170
187
|
if (!process.stdin.isTTY) {
|
|
171
188
|
return null;
|
|
@@ -217,8 +234,11 @@ async function resolvePat() {
|
|
|
217
234
|
}
|
|
218
235
|
const promptedPat = await promptForPat();
|
|
219
236
|
if (promptedPat !== null) {
|
|
220
|
-
|
|
221
|
-
|
|
237
|
+
const normalizedPat = normalizePat(promptedPat);
|
|
238
|
+
if (normalizedPat !== null) {
|
|
239
|
+
await storePat(normalizedPat);
|
|
240
|
+
return { pat: normalizedPat, source: "prompt" };
|
|
241
|
+
}
|
|
222
242
|
}
|
|
223
243
|
throw new Error(
|
|
224
244
|
"Authentication cancelled. Set AZDO_PAT environment variable or run again to enter a PAT."
|
|
@@ -351,6 +371,86 @@ function unsetConfigValue(key) {
|
|
|
351
371
|
saveConfig(config);
|
|
352
372
|
}
|
|
353
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
|
+
|
|
354
454
|
// src/commands/get-item.ts
|
|
355
455
|
function stripHtml(html) {
|
|
356
456
|
let text = html;
|
|
@@ -405,81 +505,18 @@ function createGetItemCommand() {
|
|
|
405
505
|
const command = new Command("get-item");
|
|
406
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(
|
|
407
507
|
async (idStr, options) => {
|
|
408
|
-
const id =
|
|
409
|
-
|
|
410
|
-
process.stderr.write(
|
|
411
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
412
|
-
`
|
|
413
|
-
);
|
|
414
|
-
process.exit(1);
|
|
415
|
-
}
|
|
416
|
-
const hasOrg = options.org !== void 0;
|
|
417
|
-
const hasProject = options.project !== void 0;
|
|
418
|
-
if (hasOrg !== hasProject) {
|
|
419
|
-
process.stderr.write(
|
|
420
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
421
|
-
);
|
|
422
|
-
process.exit(1);
|
|
423
|
-
}
|
|
508
|
+
const id = parseWorkItemId(idStr);
|
|
509
|
+
validateOrgProjectPair(options);
|
|
424
510
|
let context;
|
|
425
511
|
try {
|
|
426
|
-
|
|
427
|
-
context = { org: options.org, project: options.project };
|
|
428
|
-
} else {
|
|
429
|
-
const config = loadConfig();
|
|
430
|
-
if (config.org && config.project) {
|
|
431
|
-
context = { org: config.org, project: config.project };
|
|
432
|
-
} else {
|
|
433
|
-
let gitContext = null;
|
|
434
|
-
try {
|
|
435
|
-
gitContext = detectAzdoContext();
|
|
436
|
-
} catch {
|
|
437
|
-
}
|
|
438
|
-
const org = config.org || gitContext?.org;
|
|
439
|
-
const project = config.project || gitContext?.project;
|
|
440
|
-
if (org && project) {
|
|
441
|
-
context = { org, project };
|
|
442
|
-
} else {
|
|
443
|
-
throw new Error(
|
|
444
|
-
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
512
|
+
context = resolveContext(options);
|
|
449
513
|
const credential = await resolvePat();
|
|
450
514
|
const fieldsList = options.fields ? options.fields.split(",").map((f) => f.trim()) : loadConfig().fields;
|
|
451
515
|
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
452
516
|
const output = formatWorkItem(workItem, options.short ?? false);
|
|
453
517
|
process.stdout.write(output + "\n");
|
|
454
518
|
} catch (err) {
|
|
455
|
-
|
|
456
|
-
const msg = error.message;
|
|
457
|
-
if (msg === "AUTH_FAILED") {
|
|
458
|
-
process.stderr.write(
|
|
459
|
-
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (read)" scope.\n'
|
|
460
|
-
);
|
|
461
|
-
} else if (msg === "NOT_FOUND") {
|
|
462
|
-
process.stderr.write(
|
|
463
|
-
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
464
|
-
`
|
|
465
|
-
);
|
|
466
|
-
} else if (msg === "PERMISSION_DENIED") {
|
|
467
|
-
process.stderr.write(
|
|
468
|
-
`Error: Access denied. Your PAT may lack permissions for project "${context.project}".
|
|
469
|
-
`
|
|
470
|
-
);
|
|
471
|
-
} else if (msg === "NETWORK_ERROR") {
|
|
472
|
-
process.stderr.write(
|
|
473
|
-
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
474
|
-
);
|
|
475
|
-
} else if (msg.includes("Not in a git repository") || msg.includes("is not an Azure DevOps URL") || msg.includes("Authentication cancelled")) {
|
|
476
|
-
process.stderr.write(`Error: ${msg}
|
|
477
|
-
`);
|
|
478
|
-
} else {
|
|
479
|
-
process.stderr.write(`Error: ${msg}
|
|
480
|
-
`);
|
|
481
|
-
}
|
|
482
|
-
process.exit(1);
|
|
519
|
+
handleCommandError(err, id, context, "read");
|
|
483
520
|
}
|
|
484
521
|
}
|
|
485
522
|
);
|
|
@@ -652,48 +689,12 @@ function createConfigCommand() {
|
|
|
652
689
|
|
|
653
690
|
// src/commands/set-state.ts
|
|
654
691
|
import { Command as Command4 } from "commander";
|
|
655
|
-
function resolveContext(options) {
|
|
656
|
-
if (options.org && options.project) {
|
|
657
|
-
return { org: options.org, project: options.project };
|
|
658
|
-
}
|
|
659
|
-
const config = loadConfig();
|
|
660
|
-
if (config.org && config.project) {
|
|
661
|
-
return { org: config.org, project: config.project };
|
|
662
|
-
}
|
|
663
|
-
let gitContext = null;
|
|
664
|
-
try {
|
|
665
|
-
gitContext = detectAzdoContext();
|
|
666
|
-
} catch {
|
|
667
|
-
}
|
|
668
|
-
const org = config.org || gitContext?.org;
|
|
669
|
-
const project = config.project || gitContext?.project;
|
|
670
|
-
if (org && project) {
|
|
671
|
-
return { org, project };
|
|
672
|
-
}
|
|
673
|
-
throw new Error(
|
|
674
|
-
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
692
|
function createSetStateCommand() {
|
|
678
693
|
const command = new Command4("set-state");
|
|
679
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(
|
|
680
695
|
async (idStr, state, options) => {
|
|
681
|
-
const id =
|
|
682
|
-
|
|
683
|
-
process.stderr.write(
|
|
684
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
685
|
-
`
|
|
686
|
-
);
|
|
687
|
-
process.exit(1);
|
|
688
|
-
}
|
|
689
|
-
const hasOrg = options.org !== void 0;
|
|
690
|
-
const hasProject = options.project !== void 0;
|
|
691
|
-
if (hasOrg !== hasProject) {
|
|
692
|
-
process.stderr.write(
|
|
693
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
694
|
-
);
|
|
695
|
-
process.exit(1);
|
|
696
|
-
}
|
|
696
|
+
const id = parseWorkItemId(idStr);
|
|
697
|
+
validateOrgProjectPair(options);
|
|
697
698
|
let context;
|
|
698
699
|
try {
|
|
699
700
|
context = resolveContext(options);
|
|
@@ -717,35 +718,7 @@ function createSetStateCommand() {
|
|
|
717
718
|
`);
|
|
718
719
|
}
|
|
719
720
|
} catch (err) {
|
|
720
|
-
|
|
721
|
-
const msg = error.message;
|
|
722
|
-
if (msg === "AUTH_FAILED") {
|
|
723
|
-
process.stderr.write(
|
|
724
|
-
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (Read & Write)" scope.\n'
|
|
725
|
-
);
|
|
726
|
-
} else if (msg === "PERMISSION_DENIED") {
|
|
727
|
-
process.stderr.write(
|
|
728
|
-
`Error: Access denied. Your PAT may lack write permissions for project "${context.project}".
|
|
729
|
-
`
|
|
730
|
-
);
|
|
731
|
-
} else if (msg === "NOT_FOUND") {
|
|
732
|
-
process.stderr.write(
|
|
733
|
-
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
734
|
-
`
|
|
735
|
-
);
|
|
736
|
-
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
737
|
-
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
738
|
-
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
739
|
-
`);
|
|
740
|
-
} else if (msg === "NETWORK_ERROR") {
|
|
741
|
-
process.stderr.write(
|
|
742
|
-
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
743
|
-
);
|
|
744
|
-
} else {
|
|
745
|
-
process.stderr.write(`Error: ${msg}
|
|
746
|
-
`);
|
|
747
|
-
}
|
|
748
|
-
process.exit(1);
|
|
721
|
+
handleCommandError(err, id, context, "write");
|
|
749
722
|
}
|
|
750
723
|
}
|
|
751
724
|
);
|
|
@@ -754,40 +727,11 @@ function createSetStateCommand() {
|
|
|
754
727
|
|
|
755
728
|
// src/commands/assign.ts
|
|
756
729
|
import { Command as Command5 } from "commander";
|
|
757
|
-
function resolveContext2(options) {
|
|
758
|
-
if (options.org && options.project) {
|
|
759
|
-
return { org: options.org, project: options.project };
|
|
760
|
-
}
|
|
761
|
-
const config = loadConfig();
|
|
762
|
-
if (config.org && config.project) {
|
|
763
|
-
return { org: config.org, project: config.project };
|
|
764
|
-
}
|
|
765
|
-
let gitContext = null;
|
|
766
|
-
try {
|
|
767
|
-
gitContext = detectAzdoContext();
|
|
768
|
-
} catch {
|
|
769
|
-
}
|
|
770
|
-
const org = config.org || gitContext?.org;
|
|
771
|
-
const project = config.project || gitContext?.project;
|
|
772
|
-
if (org && project) {
|
|
773
|
-
return { org, project };
|
|
774
|
-
}
|
|
775
|
-
throw new Error(
|
|
776
|
-
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
730
|
function createAssignCommand() {
|
|
780
731
|
const command = new Command5("assign");
|
|
781
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(
|
|
782
733
|
async (idStr, name, options) => {
|
|
783
|
-
const id =
|
|
784
|
-
if (!Number.isInteger(id) || id <= 0) {
|
|
785
|
-
process.stderr.write(
|
|
786
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
787
|
-
`
|
|
788
|
-
);
|
|
789
|
-
process.exit(1);
|
|
790
|
-
}
|
|
734
|
+
const id = parseWorkItemId(idStr);
|
|
791
735
|
if (!name && !options.unassign) {
|
|
792
736
|
process.stderr.write(
|
|
793
737
|
"Error: Either provide a user name or use --unassign.\n"
|
|
@@ -800,17 +744,10 @@ function createAssignCommand() {
|
|
|
800
744
|
);
|
|
801
745
|
process.exit(1);
|
|
802
746
|
}
|
|
803
|
-
|
|
804
|
-
const hasProject = options.project !== void 0;
|
|
805
|
-
if (hasOrg !== hasProject) {
|
|
806
|
-
process.stderr.write(
|
|
807
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
808
|
-
);
|
|
809
|
-
process.exit(1);
|
|
810
|
-
}
|
|
747
|
+
validateOrgProjectPair(options);
|
|
811
748
|
let context;
|
|
812
749
|
try {
|
|
813
|
-
context =
|
|
750
|
+
context = resolveContext(options);
|
|
814
751
|
const credential = await resolvePat();
|
|
815
752
|
const value = options.unassign ? "" : name;
|
|
816
753
|
const operations = [
|
|
@@ -833,35 +770,7 @@ function createAssignCommand() {
|
|
|
833
770
|
`);
|
|
834
771
|
}
|
|
835
772
|
} catch (err) {
|
|
836
|
-
|
|
837
|
-
const msg = error.message;
|
|
838
|
-
if (msg === "AUTH_FAILED") {
|
|
839
|
-
process.stderr.write(
|
|
840
|
-
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (Read & Write)" scope.\n'
|
|
841
|
-
);
|
|
842
|
-
} else if (msg === "PERMISSION_DENIED") {
|
|
843
|
-
process.stderr.write(
|
|
844
|
-
`Error: Access denied. Your PAT may lack write permissions for project "${context.project}".
|
|
845
|
-
`
|
|
846
|
-
);
|
|
847
|
-
} else if (msg === "NOT_FOUND") {
|
|
848
|
-
process.stderr.write(
|
|
849
|
-
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
850
|
-
`
|
|
851
|
-
);
|
|
852
|
-
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
853
|
-
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
854
|
-
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
855
|
-
`);
|
|
856
|
-
} else if (msg === "NETWORK_ERROR") {
|
|
857
|
-
process.stderr.write(
|
|
858
|
-
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
859
|
-
);
|
|
860
|
-
} else {
|
|
861
|
-
process.stderr.write(`Error: ${msg}
|
|
862
|
-
`);
|
|
863
|
-
}
|
|
864
|
-
process.exit(1);
|
|
773
|
+
handleCommandError(err, id, context, "write");
|
|
865
774
|
}
|
|
866
775
|
}
|
|
867
776
|
);
|
|
@@ -870,51 +779,15 @@ function createAssignCommand() {
|
|
|
870
779
|
|
|
871
780
|
// src/commands/set-field.ts
|
|
872
781
|
import { Command as Command6 } from "commander";
|
|
873
|
-
function resolveContext3(options) {
|
|
874
|
-
if (options.org && options.project) {
|
|
875
|
-
return { org: options.org, project: options.project };
|
|
876
|
-
}
|
|
877
|
-
const config = loadConfig();
|
|
878
|
-
if (config.org && config.project) {
|
|
879
|
-
return { org: config.org, project: config.project };
|
|
880
|
-
}
|
|
881
|
-
let gitContext = null;
|
|
882
|
-
try {
|
|
883
|
-
gitContext = detectAzdoContext();
|
|
884
|
-
} catch {
|
|
885
|
-
}
|
|
886
|
-
const org = config.org || gitContext?.org;
|
|
887
|
-
const project = config.project || gitContext?.project;
|
|
888
|
-
if (org && project) {
|
|
889
|
-
return { org, project };
|
|
890
|
-
}
|
|
891
|
-
throw new Error(
|
|
892
|
-
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
782
|
function createSetFieldCommand() {
|
|
896
783
|
const command = new Command6("set-field");
|
|
897
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(
|
|
898
785
|
async (idStr, field, value, options) => {
|
|
899
|
-
const id =
|
|
900
|
-
|
|
901
|
-
process.stderr.write(
|
|
902
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
903
|
-
`
|
|
904
|
-
);
|
|
905
|
-
process.exit(1);
|
|
906
|
-
}
|
|
907
|
-
const hasOrg = options.org !== void 0;
|
|
908
|
-
const hasProject = options.project !== void 0;
|
|
909
|
-
if (hasOrg !== hasProject) {
|
|
910
|
-
process.stderr.write(
|
|
911
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
912
|
-
);
|
|
913
|
-
process.exit(1);
|
|
914
|
-
}
|
|
786
|
+
const id = parseWorkItemId(idStr);
|
|
787
|
+
validateOrgProjectPair(options);
|
|
915
788
|
let context;
|
|
916
789
|
try {
|
|
917
|
-
context =
|
|
790
|
+
context = resolveContext(options);
|
|
918
791
|
const credential = await resolvePat();
|
|
919
792
|
const operations = [
|
|
920
793
|
{ op: "add", path: `/fields/${field}`, value }
|
|
@@ -935,35 +808,178 @@ function createSetFieldCommand() {
|
|
|
935
808
|
`);
|
|
936
809
|
}
|
|
937
810
|
} catch (err) {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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");
|
|
962
890
|
} else {
|
|
963
|
-
process.
|
|
964
|
-
`);
|
|
891
|
+
process.stdout.write(toMarkdown(value) + "\n");
|
|
965
892
|
}
|
|
966
|
-
|
|
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");
|
|
967
983
|
}
|
|
968
984
|
}
|
|
969
985
|
);
|
|
@@ -971,7 +987,7 @@ function createSetFieldCommand() {
|
|
|
971
987
|
}
|
|
972
988
|
|
|
973
989
|
// src/index.ts
|
|
974
|
-
var program = new
|
|
990
|
+
var program = new Command9();
|
|
975
991
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
976
992
|
program.addCommand(createGetItemCommand());
|
|
977
993
|
program.addCommand(createClearPatCommand());
|
|
@@ -979,6 +995,8 @@ program.addCommand(createConfigCommand());
|
|
|
979
995
|
program.addCommand(createSetStateCommand());
|
|
980
996
|
program.addCommand(createAssignCommand());
|
|
981
997
|
program.addCommand(createSetFieldCommand());
|
|
998
|
+
program.addCommand(createGetMdFieldCommand());
|
|
999
|
+
program.addCommand(createSetMdFieldCommand());
|
|
982
1000
|
program.showHelpAfterError();
|
|
983
1001
|
program.parse();
|
|
984
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",
|