azdo-cli 0.2.0-develop.21 → 0.2.0-develop.41
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 +385 -7
- package/package.json +1 -1
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 Command7 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -37,15 +37,18 @@ function buildExtraFields(fields, requested) {
|
|
|
37
37
|
return Object.keys(result).length > 0 ? result : null;
|
|
38
38
|
}
|
|
39
39
|
async function getWorkItem(context, id, pat, extraFields) {
|
|
40
|
-
|
|
40
|
+
const url = new URL(
|
|
41
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
42
|
+
);
|
|
43
|
+
url.searchParams.set("api-version", "7.1");
|
|
41
44
|
if (extraFields && extraFields.length > 0) {
|
|
42
45
|
const allFields = [...DEFAULT_FIELDS, ...extraFields];
|
|
43
|
-
url
|
|
46
|
+
url.searchParams.set("fields", allFields.join(","));
|
|
44
47
|
}
|
|
45
48
|
const token = Buffer.from(`:${pat}`).toString("base64");
|
|
46
49
|
let response;
|
|
47
50
|
try {
|
|
48
|
-
response = await fetch(url, {
|
|
51
|
+
response = await fetch(url.toString(), {
|
|
49
52
|
headers: {
|
|
50
53
|
Authorization: `Basic ${token}`
|
|
51
54
|
}
|
|
@@ -90,6 +93,51 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
90
93
|
extraFields: extraFields && extraFields.length > 0 ? buildExtraFields(data.fields, extraFields) : null
|
|
91
94
|
};
|
|
92
95
|
}
|
|
96
|
+
async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
97
|
+
const url = new URL(
|
|
98
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
99
|
+
);
|
|
100
|
+
url.searchParams.set("api-version", "7.1");
|
|
101
|
+
const token = Buffer.from(`:${pat}`).toString("base64");
|
|
102
|
+
let response;
|
|
103
|
+
try {
|
|
104
|
+
response = await fetch(url.toString(), {
|
|
105
|
+
method: "PATCH",
|
|
106
|
+
headers: {
|
|
107
|
+
Authorization: `Basic ${token}`,
|
|
108
|
+
"Content-Type": "application/json-patch+json"
|
|
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");
|
|
118
|
+
if (response.status === 400) {
|
|
119
|
+
let serverMessage = "Unknown error";
|
|
120
|
+
try {
|
|
121
|
+
const body = await response.json();
|
|
122
|
+
if (body.message) serverMessage = body.message;
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`UPDATE_REJECTED: ${serverMessage}`);
|
|
126
|
+
}
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(`HTTP_${response.status}`);
|
|
129
|
+
}
|
|
130
|
+
const data = await response.json();
|
|
131
|
+
const lastOp = operations[operations.length - 1];
|
|
132
|
+
const fieldValue = lastOp.value ?? null;
|
|
133
|
+
return {
|
|
134
|
+
id: data.id,
|
|
135
|
+
rev: data.rev,
|
|
136
|
+
title: data.fields["System.Title"],
|
|
137
|
+
fieldName,
|
|
138
|
+
fieldValue
|
|
139
|
+
};
|
|
140
|
+
}
|
|
93
141
|
|
|
94
142
|
// src/services/auth.ts
|
|
95
143
|
import { createInterface } from "readline";
|
|
@@ -124,6 +172,10 @@ async function deletePat() {
|
|
|
124
172
|
}
|
|
125
173
|
|
|
126
174
|
// src/services/auth.ts
|
|
175
|
+
function normalizePat(rawPat) {
|
|
176
|
+
const trimmedPat = rawPat.trim();
|
|
177
|
+
return trimmedPat.length > 0 ? trimmedPat : null;
|
|
178
|
+
}
|
|
127
179
|
async function promptForPat() {
|
|
128
180
|
if (!process.stdin.isTTY) {
|
|
129
181
|
return null;
|
|
@@ -175,8 +227,11 @@ async function resolvePat() {
|
|
|
175
227
|
}
|
|
176
228
|
const promptedPat = await promptForPat();
|
|
177
229
|
if (promptedPat !== null) {
|
|
178
|
-
|
|
179
|
-
|
|
230
|
+
const normalizedPat = normalizePat(promptedPat);
|
|
231
|
+
if (normalizedPat !== null) {
|
|
232
|
+
await storePat(normalizedPat);
|
|
233
|
+
return { pat: normalizedPat, source: "prompt" };
|
|
234
|
+
}
|
|
180
235
|
}
|
|
181
236
|
throw new Error(
|
|
182
237
|
"Authentication cancelled. Set AZDO_PAT environment variable or run again to enter a PAT."
|
|
@@ -608,12 +663,335 @@ function createConfigCommand() {
|
|
|
608
663
|
return config;
|
|
609
664
|
}
|
|
610
665
|
|
|
666
|
+
// src/commands/set-state.ts
|
|
667
|
+
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
|
+
function createSetStateCommand() {
|
|
691
|
+
const command = new Command4("set-state");
|
|
692
|
+
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
|
+
async (idStr, state, options) => {
|
|
694
|
+
const id = parseInt(idStr, 10);
|
|
695
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
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
|
+
}
|
|
710
|
+
let context;
|
|
711
|
+
try {
|
|
712
|
+
context = resolveContext(options);
|
|
713
|
+
const credential = await resolvePat();
|
|
714
|
+
const operations = [
|
|
715
|
+
{ op: "add", path: "/fields/System.State", value: state }
|
|
716
|
+
];
|
|
717
|
+
const result = await updateWorkItem(context, id, credential.pat, "System.State", operations);
|
|
718
|
+
if (options.json) {
|
|
719
|
+
process.stdout.write(
|
|
720
|
+
JSON.stringify({
|
|
721
|
+
id: result.id,
|
|
722
|
+
rev: result.rev,
|
|
723
|
+
title: result.title,
|
|
724
|
+
field: result.fieldName,
|
|
725
|
+
value: result.fieldValue
|
|
726
|
+
}) + "\n"
|
|
727
|
+
);
|
|
728
|
+
} else {
|
|
729
|
+
process.stdout.write(`Updated work item ${result.id}: State -> ${state}
|
|
730
|
+
`);
|
|
731
|
+
}
|
|
732
|
+
} catch (err) {
|
|
733
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
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);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
);
|
|
765
|
+
return command;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/commands/assign.ts
|
|
769
|
+
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
|
+
function createAssignCommand() {
|
|
793
|
+
const command = new Command5("assign");
|
|
794
|
+
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
|
+
async (idStr, name, options) => {
|
|
796
|
+
const id = parseInt(idStr, 10);
|
|
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
|
+
}
|
|
804
|
+
if (!name && !options.unassign) {
|
|
805
|
+
process.stderr.write(
|
|
806
|
+
"Error: Either provide a user name or use --unassign.\n"
|
|
807
|
+
);
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
if (name && options.unassign) {
|
|
811
|
+
process.stderr.write(
|
|
812
|
+
"Error: Cannot provide both a user name and --unassign.\n"
|
|
813
|
+
);
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
const hasOrg = options.org !== void 0;
|
|
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
|
+
}
|
|
824
|
+
let context;
|
|
825
|
+
try {
|
|
826
|
+
context = resolveContext2(options);
|
|
827
|
+
const credential = await resolvePat();
|
|
828
|
+
const value = options.unassign ? "" : name;
|
|
829
|
+
const operations = [
|
|
830
|
+
{ op: "add", path: "/fields/System.AssignedTo", value }
|
|
831
|
+
];
|
|
832
|
+
const result = await updateWorkItem(context, id, credential.pat, "System.AssignedTo", operations);
|
|
833
|
+
if (options.json) {
|
|
834
|
+
process.stdout.write(
|
|
835
|
+
JSON.stringify({
|
|
836
|
+
id: result.id,
|
|
837
|
+
rev: result.rev,
|
|
838
|
+
title: result.title,
|
|
839
|
+
field: result.fieldName,
|
|
840
|
+
value: result.fieldValue
|
|
841
|
+
}) + "\n"
|
|
842
|
+
);
|
|
843
|
+
} else {
|
|
844
|
+
const displayValue = options.unassign ? "(unassigned)" : name;
|
|
845
|
+
process.stdout.write(`Updated work item ${result.id}: Assigned To -> ${displayValue}
|
|
846
|
+
`);
|
|
847
|
+
}
|
|
848
|
+
} catch (err) {
|
|
849
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
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);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
);
|
|
881
|
+
return command;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/commands/set-field.ts
|
|
885
|
+
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
|
+
function createSetFieldCommand() {
|
|
909
|
+
const command = new Command6("set-field");
|
|
910
|
+
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
|
+
async (idStr, field, value, options) => {
|
|
912
|
+
const id = parseInt(idStr, 10);
|
|
913
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
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
|
+
}
|
|
928
|
+
let context;
|
|
929
|
+
try {
|
|
930
|
+
context = resolveContext3(options);
|
|
931
|
+
const credential = await resolvePat();
|
|
932
|
+
const operations = [
|
|
933
|
+
{ op: "add", path: `/fields/${field}`, value }
|
|
934
|
+
];
|
|
935
|
+
const result = await updateWorkItem(context, id, credential.pat, field, operations);
|
|
936
|
+
if (options.json) {
|
|
937
|
+
process.stdout.write(
|
|
938
|
+
JSON.stringify({
|
|
939
|
+
id: result.id,
|
|
940
|
+
rev: result.rev,
|
|
941
|
+
title: result.title,
|
|
942
|
+
field: result.fieldName,
|
|
943
|
+
value: result.fieldValue
|
|
944
|
+
}) + "\n"
|
|
945
|
+
);
|
|
946
|
+
} else {
|
|
947
|
+
process.stdout.write(`Updated work item ${result.id}: ${field} -> ${value}
|
|
948
|
+
`);
|
|
949
|
+
}
|
|
950
|
+
} catch (err) {
|
|
951
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
952
|
+
const msg = error.message;
|
|
953
|
+
if (msg === "AUTH_FAILED") {
|
|
954
|
+
process.stderr.write(
|
|
955
|
+
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (Read & Write)" scope.\n'
|
|
956
|
+
);
|
|
957
|
+
} else if (msg === "PERMISSION_DENIED") {
|
|
958
|
+
process.stderr.write(
|
|
959
|
+
`Error: Access denied. Your PAT may lack write permissions for project "${context.project}".
|
|
960
|
+
`
|
|
961
|
+
);
|
|
962
|
+
} else if (msg === "NOT_FOUND") {
|
|
963
|
+
process.stderr.write(
|
|
964
|
+
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
965
|
+
`
|
|
966
|
+
);
|
|
967
|
+
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
968
|
+
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
969
|
+
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
970
|
+
`);
|
|
971
|
+
} else if (msg === "NETWORK_ERROR") {
|
|
972
|
+
process.stderr.write(
|
|
973
|
+
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
974
|
+
);
|
|
975
|
+
} else {
|
|
976
|
+
process.stderr.write(`Error: ${msg}
|
|
977
|
+
`);
|
|
978
|
+
}
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
);
|
|
983
|
+
return command;
|
|
984
|
+
}
|
|
985
|
+
|
|
611
986
|
// src/index.ts
|
|
612
|
-
var program = new
|
|
987
|
+
var program = new Command7();
|
|
613
988
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
614
989
|
program.addCommand(createGetItemCommand());
|
|
615
990
|
program.addCommand(createClearPatCommand());
|
|
616
991
|
program.addCommand(createConfigCommand());
|
|
992
|
+
program.addCommand(createSetStateCommand());
|
|
993
|
+
program.addCommand(createAssignCommand());
|
|
994
|
+
program.addCommand(createSetFieldCommand());
|
|
617
995
|
program.showHelpAfterError();
|
|
618
996
|
program.parse();
|
|
619
997
|
if (process.argv.length <= 2) {
|