azdo-cli 0.2.0-develop.41 → 0.2.0-develop.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +306 -301
  2. 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 Command7 } from "commander";
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 token = Buffer.from(`:${pat}`).toString("base64");
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 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");
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 = parseInt(idStr, 10);
422
- if (!Number.isInteger(id) || id <= 0) {
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
- if (options.org && options.project) {
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
- const error = err instanceof Error ? err : new Error(String(err));
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 = 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
- }
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
- 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);
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 = 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
- }
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
- 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
- }
747
+ validateOrgProjectPair(options);
824
748
  let context;
825
749
  try {
826
- context = resolveContext2(options);
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
- 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);
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 = 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
- }
786
+ const id = parseWorkItemId(idStr);
787
+ validateOrgProjectPair(options);
928
788
  let context;
929
789
  try {
930
- context = resolveContext3(options);
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
- 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
- );
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.stderr.write(`Error: ${msg}
977
- `);
891
+ process.stdout.write(toMarkdown(value) + "\n");
978
892
  }
979
- process.exit(1);
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 Command7();
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.41",
3
+ "version": "0.2.0-develop.48",
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",