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.
Files changed (2) hide show
  1. package/dist/index.js +324 -306
  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) {
@@ -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
- let url = `https://dev.azure.com/${context.org}/${context.project}/_apis/wit/workitems/${id}?api-version=7.1`;
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 += `&fields=${allFields.join(",")}`;
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
- if (response.status === 401) throw new Error("AUTH_FAILED");
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 updateWorkItem(context, id, pat, fieldName, operations) {
94
- const url = `https://dev.azure.com/${context.org}/${context.project}/_apis/wit/workitems/${id}?api-version=7.1`;
95
- const token = Buffer.from(`:${pat}`).toString("base64");
96
- let response;
97
- try {
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
- if (response.status === 401) throw new Error("AUTH_FAILED");
110
- if (response.status === 403) throw new Error("PERMISSION_DENIED");
111
- if (response.status === 404) throw new Error("NOT_FOUND");
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
- await storePat(promptedPat);
221
- return { pat: promptedPat, source: "prompt" };
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 = parseInt(idStr, 10);
409
- if (!Number.isInteger(id) || id <= 0) {
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
- if (options.org && options.project) {
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
- const error = err instanceof Error ? err : new Error(String(err));
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 = parseInt(idStr, 10);
682
- if (!Number.isInteger(id) || id <= 0) {
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
- const error = err instanceof Error ? err : new Error(String(err));
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 = parseInt(idStr, 10);
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
- const hasOrg = options.org !== void 0;
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 = resolveContext2(options);
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
- const error = err instanceof Error ? err : new Error(String(err));
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 = parseInt(idStr, 10);
900
- if (!Number.isInteger(id) || id <= 0) {
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 = resolveContext3(options);
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
- const error = err instanceof Error ? err : new Error(String(err));
939
- const msg = error.message;
940
- if (msg === "AUTH_FAILED") {
941
- process.stderr.write(
942
- 'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (Read & Write)" scope.\n'
943
- );
944
- } else if (msg === "PERMISSION_DENIED") {
945
- process.stderr.write(
946
- `Error: Access denied. Your PAT may lack write permissions for project "${context.project}".
947
- `
948
- );
949
- } else if (msg === "NOT_FOUND") {
950
- process.stderr.write(
951
- `Error: Work item ${id} not found in ${context.org}/${context.project}.
952
- `
953
- );
954
- } else if (msg.startsWith("UPDATE_REJECTED:")) {
955
- const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
956
- process.stderr.write(`Error: Update rejected: ${serverMsg}
957
- `);
958
- } else if (msg === "NETWORK_ERROR") {
959
- process.stderr.write(
960
- "Error: Could not connect to Azure DevOps. Check your network connection.\n"
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.stderr.write(`Error: ${msg}
964
- `);
891
+ process.stdout.write(toMarkdown(value) + "\n");
965
892
  }
966
- 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");
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 Command7();
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.24",
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",