azdo-cli 0.2.0-develop.14 → 0.2.0-develop.141

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 (3) hide show
  1. package/README.md +299 -26
  2. package/dist/index.js +1673 -103
  3. package/package.json +10 -7
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 Command3 } from "commander";
4
+ import { Command as Command12 } from "commander";
5
5
 
6
6
  // src/version.ts
7
7
  import { readFileSync } from "fs";
@@ -15,22 +15,138 @@ var version = pkg.version;
15
15
  import { Command } from "commander";
16
16
 
17
17
  // src/services/azdo-client.ts
18
- async function getWorkItem(context, id, pat) {
19
- const url = `https://dev.azure.com/${context.org}/${context.project}/_apis/wit/workitems/${id}?api-version=7.1`;
18
+ var DEFAULT_FIELDS = [
19
+ "System.Title",
20
+ "System.State",
21
+ "System.WorkItemType",
22
+ "System.AssignedTo",
23
+ "System.Description",
24
+ "Microsoft.VSTS.Common.AcceptanceCriteria",
25
+ "Microsoft.VSTS.TCM.ReproSteps",
26
+ "System.AreaPath",
27
+ "System.IterationPath"
28
+ ];
29
+ function authHeaders(pat) {
20
30
  const token = Buffer.from(`:${pat}`).toString("base64");
31
+ return { Authorization: `Basic ${token}` };
32
+ }
33
+ async function fetchWithErrors(url, init) {
21
34
  let response;
22
35
  try {
23
- response = await fetch(url, {
24
- headers: {
25
- Authorization: `Basic ${token}`
26
- }
27
- });
36
+ response = await fetch(url, init);
28
37
  } catch {
29
38
  throw new Error("NETWORK_ERROR");
30
39
  }
31
40
  if (response.status === 401) throw new Error("AUTH_FAILED");
32
41
  if (response.status === 403) throw new Error("PERMISSION_DENIED");
33
- if (response.status === 404) throw new Error("NOT_FOUND");
42
+ if (response.status === 404) {
43
+ let detail = "";
44
+ try {
45
+ const body = await response.text();
46
+ detail = ` | url=${url} | body=${body}`;
47
+ } catch {
48
+ }
49
+ throw new Error(`NOT_FOUND${detail}`);
50
+ }
51
+ return response;
52
+ }
53
+ async function readResponseMessage(response) {
54
+ try {
55
+ const body = await response.json();
56
+ if (typeof body.message === "string" && body.message.trim() !== "") {
57
+ return body.message.trim();
58
+ }
59
+ } catch {
60
+ }
61
+ return null;
62
+ }
63
+ function normalizeFieldList(fields) {
64
+ return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
65
+ }
66
+ function stringifyFieldValue(value) {
67
+ if (typeof value === "object" && value !== null) {
68
+ return JSON.stringify(value);
69
+ }
70
+ return String(value);
71
+ }
72
+ function buildExtraFields(fields, requested) {
73
+ const result = {};
74
+ for (const name of requested) {
75
+ let val = fields[name];
76
+ let resolvedName = name;
77
+ if (val === void 0) {
78
+ const nameSuffix = name.split(".").pop().toLowerCase();
79
+ const match = Object.keys(fields).find(
80
+ (k) => k.split(".").pop().toLowerCase() === nameSuffix
81
+ );
82
+ if (match !== void 0) {
83
+ val = fields[match];
84
+ resolvedName = match;
85
+ }
86
+ }
87
+ if (val !== void 0 && val !== null) {
88
+ result[resolvedName] = stringifyFieldValue(val);
89
+ }
90
+ }
91
+ return Object.keys(result).length > 0 ? result : null;
92
+ }
93
+ function writeHeaders(pat) {
94
+ return {
95
+ ...authHeaders(pat),
96
+ "Content-Type": "application/json-patch+json"
97
+ };
98
+ }
99
+ async function readWriteResponse(response, errorCode) {
100
+ if (response.status === 400) {
101
+ const serverMessage = await readResponseMessage(response) ?? "Unknown error";
102
+ throw new Error(`${errorCode}: ${serverMessage}`);
103
+ }
104
+ if (!response.ok) {
105
+ throw new Error(`HTTP_${response.status}`);
106
+ }
107
+ const data = await response.json();
108
+ return {
109
+ id: data.id,
110
+ rev: data.rev,
111
+ fields: data.fields
112
+ };
113
+ }
114
+ async function getWorkItemFields(context, id, pat) {
115
+ const url = new URL(
116
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
117
+ );
118
+ url.searchParams.set("api-version", "7.1");
119
+ url.searchParams.set("$expand", "all");
120
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
121
+ if (response.status === 400) {
122
+ const serverMessage = await readResponseMessage(response);
123
+ if (serverMessage) {
124
+ throw new Error(`BAD_REQUEST: ${serverMessage}`);
125
+ }
126
+ }
127
+ if (!response.ok) {
128
+ throw new Error(`HTTP_${response.status}`);
129
+ }
130
+ const data = await response.json();
131
+ return data.fields;
132
+ }
133
+ async function getWorkItem(context, id, pat, extraFields) {
134
+ const url = new URL(
135
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
136
+ );
137
+ url.searchParams.set("api-version", "7.1");
138
+ const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
139
+ if (normalizedExtraFields.length > 0) {
140
+ const allFields = normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields]);
141
+ url.searchParams.set("fields", allFields.join(","));
142
+ }
143
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
144
+ if (response.status === 400) {
145
+ const serverMessage = await readResponseMessage(response);
146
+ if (serverMessage) {
147
+ throw new Error(`BAD_REQUEST: ${serverMessage}`);
148
+ }
149
+ }
34
150
  if (!response.ok) {
35
151
  throw new Error(`HTTP_${response.status}`);
36
152
  }
@@ -47,7 +163,7 @@ async function getWorkItem(context, id, pat) {
47
163
  }
48
164
  let combinedDescription = null;
49
165
  if (descriptionParts.length === 1) {
50
- combinedDescription = descriptionParts[0].value;
166
+ combinedDescription = descriptionParts.at(0)?.value ?? null;
51
167
  } else if (descriptionParts.length > 1) {
52
168
  combinedDescription = descriptionParts.map((p) => `<h3>${p.label}</h3>${p.value}`).join("");
53
169
  }
@@ -61,9 +177,70 @@ async function getWorkItem(context, id, pat) {
61
177
  description: combinedDescription,
62
178
  areaPath: data.fields["System.AreaPath"],
63
179
  iterationPath: data.fields["System.IterationPath"],
64
- url: data._links.html.href
180
+ url: data._links.html.href,
181
+ extraFields: normalizedExtraFields.length > 0 ? buildExtraFields(data.fields, normalizedExtraFields) : null
182
+ };
183
+ }
184
+ async function getWorkItemFieldValue(context, id, pat, fieldName) {
185
+ const url = new URL(
186
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
187
+ );
188
+ url.searchParams.set("api-version", "7.1");
189
+ url.searchParams.set("fields", fieldName);
190
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
191
+ if (response.status === 400) {
192
+ const serverMessage = await readResponseMessage(response);
193
+ if (serverMessage) {
194
+ throw new Error(`BAD_REQUEST: ${serverMessage}`);
195
+ }
196
+ }
197
+ if (!response.ok) {
198
+ throw new Error(`HTTP_${response.status}`);
199
+ }
200
+ const data = await response.json();
201
+ const value = data.fields[fieldName];
202
+ if (value === void 0 || value === null || value === "") {
203
+ return null;
204
+ }
205
+ return stringifyFieldValue(value);
206
+ }
207
+ async function updateWorkItem(context, id, pat, fieldName, operations) {
208
+ const result = await applyWorkItemPatch(context, id, pat, operations);
209
+ const title = result.fields["System.Title"];
210
+ const lastOp = operations.at(-1);
211
+ const fieldValue = lastOp?.value ?? null;
212
+ return {
213
+ id: result.id,
214
+ rev: result.rev,
215
+ title: typeof title === "string" ? title : "",
216
+ fieldName,
217
+ fieldValue
65
218
  };
66
219
  }
220
+ async function createWorkItem(context, workItemType, pat, operations) {
221
+ const url = new URL(
222
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/$${encodeURIComponent(workItemType)}`
223
+ );
224
+ url.searchParams.set("api-version", "7.1");
225
+ const response = await fetchWithErrors(url.toString(), {
226
+ method: "POST",
227
+ headers: writeHeaders(pat),
228
+ body: JSON.stringify(operations)
229
+ });
230
+ return readWriteResponse(response, "CREATE_REJECTED");
231
+ }
232
+ async function applyWorkItemPatch(context, id, pat, operations) {
233
+ const url = new URL(
234
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
235
+ );
236
+ url.searchParams.set("api-version", "7.1");
237
+ const response = await fetchWithErrors(url.toString(), {
238
+ method: "PATCH",
239
+ headers: writeHeaders(pat),
240
+ body: JSON.stringify(operations)
241
+ });
242
+ return readWriteResponse(response, "UPDATE_REJECTED");
243
+ }
67
244
 
68
245
  // src/services/auth.ts
69
246
  import { createInterface } from "readline";
@@ -98,6 +275,10 @@ async function deletePat() {
98
275
  }
99
276
 
100
277
  // src/services/auth.ts
278
+ function normalizePat(rawPat) {
279
+ const trimmedPat = rawPat.trim();
280
+ return trimmedPat.length > 0 ? trimmedPat : null;
281
+ }
101
282
  async function promptForPat() {
102
283
  if (!process.stdin.isTTY) {
103
284
  return null;
@@ -149,8 +330,11 @@ async function resolvePat() {
149
330
  }
150
331
  const promptedPat = await promptForPat();
151
332
  if (promptedPat !== null) {
152
- await storePat(promptedPat);
153
- return { pat: promptedPat, source: "prompt" };
333
+ const normalizedPat = normalizePat(promptedPat);
334
+ if (normalizedPat !== null) {
335
+ await storePat(normalizedPat);
336
+ return { pat: normalizedPat, source: "prompt" };
337
+ }
154
338
  }
155
339
  throw new Error(
156
340
  "Authentication cancelled. Set AZDO_PAT environment variable or run again to enter a PAT."
@@ -161,21 +345,25 @@ async function resolvePat() {
161
345
  import { execSync } from "child_process";
162
346
  var patterns = [
163
347
  // HTTPS (current): https://dev.azure.com/{org}/{project}/_git/{repo}
164
- /^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/.+$/,
348
+ /^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+)$/,
165
349
  // HTTPS (legacy + DefaultCollection): https://{org}.visualstudio.com/DefaultCollection/{project}/_git/{repo}
166
- /^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/.+$/,
350
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/([^/]+)$/,
167
351
  // HTTPS (legacy): https://{org}.visualstudio.com/{project}/_git/{repo}
168
- /^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/.+$/,
352
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]+)$/,
169
353
  // SSH (current): git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
170
- /^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/.+$/,
354
+ /^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+)$/,
171
355
  // SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
172
- /^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/.+$/
356
+ /^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+)$/
173
357
  ];
174
358
  function parseAzdoRemote(url) {
175
359
  for (const pattern of patterns) {
176
- const match = url.match(pattern);
360
+ const match = pattern.exec(url);
177
361
  if (match) {
178
- return { org: match[1], project: match[2] };
362
+ const project = match[2];
363
+ if (/^DefaultCollection$/i.test(project)) {
364
+ return { org: match[1], project: "" };
365
+ }
366
+ return { org: match[1], project };
179
367
  }
180
368
  }
181
369
  return null;
@@ -188,116 +376,388 @@ function detectAzdoContext() {
188
376
  throw new Error("Not in a git repository. Provide --org and --project explicitly.");
189
377
  }
190
378
  const context = parseAzdoRemote(remoteUrl);
191
- if (!context) {
379
+ if (!context || !context.org && !context.project) {
192
380
  throw new Error('Git remote "origin" is not an Azure DevOps URL. Provide --org and --project explicitly.');
193
381
  }
194
382
  return context;
195
383
  }
384
+ function parseRepoName(url) {
385
+ for (const pattern of patterns) {
386
+ const match = pattern.exec(url);
387
+ if (match) {
388
+ return match[3];
389
+ }
390
+ }
391
+ return null;
392
+ }
393
+ function detectRepoName() {
394
+ let remoteUrl;
395
+ try {
396
+ remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
397
+ } catch {
398
+ throw new Error('Not in a git repository. Check that git remote "origin" exists and try again.');
399
+ }
400
+ const repo = parseRepoName(remoteUrl);
401
+ if (!repo) {
402
+ throw new Error('Git remote "origin" is not an Azure DevOps URL. Check that origin points to Azure DevOps and try again.');
403
+ }
404
+ return repo;
405
+ }
406
+ function getCurrentBranch() {
407
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
408
+ if (branch === "HEAD") {
409
+ throw new Error("Not on a named branch. Check out a named branch and try again.");
410
+ }
411
+ return branch;
412
+ }
413
+
414
+ // src/services/config-store.ts
415
+ import fs from "fs";
416
+ import path from "path";
417
+ import os from "os";
418
+ var SETTINGS = [
419
+ {
420
+ key: "org",
421
+ description: "Azure DevOps organization name",
422
+ type: "string",
423
+ example: "mycompany",
424
+ required: true
425
+ },
426
+ {
427
+ key: "project",
428
+ description: "Azure DevOps project name",
429
+ type: "string",
430
+ example: "MyProject",
431
+ required: true
432
+ },
433
+ {
434
+ key: "fields",
435
+ description: "Extra work item fields to include (comma-separated reference names)",
436
+ type: "string[]",
437
+ example: "System.Tags,Custom.Priority",
438
+ required: false
439
+ },
440
+ {
441
+ key: "markdown",
442
+ description: "Convert rich text fields to markdown on display",
443
+ type: "boolean",
444
+ example: "true",
445
+ required: false
446
+ }
447
+ ];
448
+ var VALID_KEYS = SETTINGS.map((s) => s.key);
449
+ function getConfigPath() {
450
+ return path.join(os.homedir(), ".azdo", "config.json");
451
+ }
452
+ function loadConfig() {
453
+ const configPath = getConfigPath();
454
+ let raw;
455
+ try {
456
+ raw = fs.readFileSync(configPath, "utf-8");
457
+ } catch (err) {
458
+ if (err.code === "ENOENT") {
459
+ return {};
460
+ }
461
+ throw err;
462
+ }
463
+ try {
464
+ return JSON.parse(raw);
465
+ } catch {
466
+ process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
467
+ `);
468
+ return {};
469
+ }
470
+ }
471
+ function saveConfig(config) {
472
+ const configPath = getConfigPath();
473
+ const dir = path.dirname(configPath);
474
+ fs.mkdirSync(dir, { recursive: true });
475
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
476
+ }
477
+ function validateKey(key) {
478
+ if (!VALID_KEYS.includes(key)) {
479
+ throw new Error(`Unknown setting key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
480
+ }
481
+ }
482
+ function getConfigValue(key) {
483
+ validateKey(key);
484
+ const config = loadConfig();
485
+ return config[key];
486
+ }
487
+ function setConfigValue(key, value) {
488
+ validateKey(key);
489
+ const config = loadConfig();
490
+ if (value === "") {
491
+ delete config[key];
492
+ } else if (key === "markdown") {
493
+ if (value !== "true" && value !== "false") {
494
+ throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
495
+ }
496
+ config.markdown = value === "true";
497
+ } else if (key === "fields") {
498
+ config.fields = value.split(",").map((s) => s.trim());
499
+ } else {
500
+ config[key] = value;
501
+ }
502
+ saveConfig(config);
503
+ }
504
+ function unsetConfigValue(key) {
505
+ validateKey(key);
506
+ const config = loadConfig();
507
+ delete config[key];
508
+ saveConfig(config);
509
+ }
510
+
511
+ // src/services/context.ts
512
+ function resolveContext(options) {
513
+ if (options.org && options.project) {
514
+ return { org: options.org, project: options.project };
515
+ }
516
+ const config = loadConfig();
517
+ if (config.org && config.project) {
518
+ return { org: config.org, project: config.project };
519
+ }
520
+ let gitContext = null;
521
+ try {
522
+ gitContext = detectAzdoContext();
523
+ } catch {
524
+ }
525
+ const org = config.org || gitContext?.org;
526
+ const project = config.project || gitContext?.project;
527
+ if (org && project) {
528
+ return { org, project };
529
+ }
530
+ throw new Error(
531
+ 'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
532
+ );
533
+ }
534
+
535
+ // src/services/md-convert.ts
536
+ import { NodeHtmlMarkdown } from "node-html-markdown";
537
+
538
+ // src/services/html-detect.ts
539
+ var HTML_TAG_REGEX = /<\/?([a-z][a-z0-9]*)\b/gi;
540
+ var HTML_TAGS = /* @__PURE__ */ new Set([
541
+ "p",
542
+ "br",
543
+ "div",
544
+ "span",
545
+ "strong",
546
+ "em",
547
+ "b",
548
+ "i",
549
+ "u",
550
+ "a",
551
+ "ul",
552
+ "ol",
553
+ "li",
554
+ "h1",
555
+ "h2",
556
+ "h3",
557
+ "h4",
558
+ "h5",
559
+ "h6",
560
+ "table",
561
+ "tr",
562
+ "td",
563
+ "th",
564
+ "img",
565
+ "pre",
566
+ "code"
567
+ ]);
568
+ function isHtml(content) {
569
+ let match;
570
+ HTML_TAG_REGEX.lastIndex = 0;
571
+ while ((match = HTML_TAG_REGEX.exec(content)) !== null) {
572
+ if (HTML_TAGS.has(match[1].toLowerCase())) {
573
+ return true;
574
+ }
575
+ }
576
+ return false;
577
+ }
578
+
579
+ // src/services/md-convert.ts
580
+ function htmlToMarkdown(html) {
581
+ return NodeHtmlMarkdown.translate(html);
582
+ }
583
+ function toMarkdown(content) {
584
+ if (isHtml(content)) {
585
+ return htmlToMarkdown(content);
586
+ }
587
+ return content;
588
+ }
589
+
590
+ // src/services/command-helpers.ts
591
+ function parseWorkItemId(idStr) {
592
+ const id = Number.parseInt(idStr, 10);
593
+ if (!Number.isInteger(id) || id <= 0) {
594
+ process.stderr.write(
595
+ `Error: Work item ID must be a positive integer. Got: "${idStr}"
596
+ `
597
+ );
598
+ process.exit(1);
599
+ }
600
+ return id;
601
+ }
602
+ function validateOrgProjectPair(options) {
603
+ const hasOrg = options.org !== void 0;
604
+ const hasProject = options.project !== void 0;
605
+ if (hasOrg !== hasProject) {
606
+ process.stderr.write(
607
+ "Error: --org and --project must both be provided, or both omitted.\n"
608
+ );
609
+ process.exit(1);
610
+ }
611
+ }
612
+ function validateSource(options) {
613
+ const hasContent = options.content !== void 0;
614
+ const hasFile = options.file !== void 0;
615
+ if (hasContent === hasFile) {
616
+ process.stderr.write("Error: provide exactly one of --content or --file\n");
617
+ process.exit(1);
618
+ }
619
+ }
620
+ function formatCreateError(err) {
621
+ const error = err instanceof Error ? err : new Error(String(err));
622
+ const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
623
+ const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
624
+ if (requiredMatches.length > 0) {
625
+ const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
626
+ return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
627
+ }
628
+ return `Create rejected: ${message}`;
629
+ }
630
+ function handleCommandError(err, id, context, scope = "write", exit = true) {
631
+ const error = err instanceof Error ? err : new Error(String(err));
632
+ const msg = error.message;
633
+ const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
634
+ if (msg === "AUTH_FAILED") {
635
+ process.stderr.write(
636
+ `Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
637
+ `
638
+ );
639
+ } else if (msg === "PERMISSION_DENIED") {
640
+ process.stderr.write(
641
+ `Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
642
+ `
643
+ );
644
+ } else if (msg.startsWith("NOT_FOUND")) {
645
+ process.stderr.write(
646
+ `Error: Work item ${id} not found in ${context?.org}/${context?.project}.
647
+ `
648
+ );
649
+ } else if (msg === "NETWORK_ERROR") {
650
+ process.stderr.write(
651
+ "Error: Could not connect to Azure DevOps. Check your network connection.\n"
652
+ );
653
+ } else if (msg.startsWith("BAD_REQUEST:")) {
654
+ const serverMsg = msg.replace("BAD_REQUEST: ", "");
655
+ process.stderr.write(`Error: Request rejected: ${serverMsg}
656
+ `);
657
+ } else if (msg.startsWith("CREATE_REJECTED:")) {
658
+ process.stderr.write(`Error: ${formatCreateError(error)}
659
+ `);
660
+ } else if (msg.startsWith("UPDATE_REJECTED:")) {
661
+ const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
662
+ process.stderr.write(`Error: Update rejected: ${serverMsg}
663
+ `);
664
+ } else {
665
+ process.stderr.write(`Error: ${msg}
666
+ `);
667
+ }
668
+ if (exit) {
669
+ process.exit(1);
670
+ } else {
671
+ process.exitCode = 1;
672
+ }
673
+ }
196
674
 
197
675
  // src/commands/get-item.ts
676
+ function parseRequestedFields(raw) {
677
+ if (raw === void 0) return void 0;
678
+ const source = Array.isArray(raw) ? raw : [raw];
679
+ const tokens = source.flatMap((entry) => entry.split(/[,\s]+/)).map((field) => field.trim()).filter((field) => field.length > 0);
680
+ if (tokens.length === 0) return void 0;
681
+ return Array.from(new Set(tokens));
682
+ }
198
683
  function stripHtml(html) {
199
684
  let text = html;
200
- text = text.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
201
- text = text.replace(/<br\s*\/?>/gi, "\n");
202
- text = text.replace(/<\/?(p|div)>/gi, "\n");
203
- text = text.replace(/<li>/gi, "\n");
204
- text = text.replace(/<[^>]*>/g, "");
205
- text = text.replace(/&amp;/g, "&");
206
- text = text.replace(/&lt;/g, "<");
207
- text = text.replace(/&gt;/g, ">");
208
- text = text.replace(/&quot;/g, '"');
209
- text = text.replace(/&#39;/g, "'");
210
- text = text.replace(/&nbsp;/g, " ");
211
- text = text.replace(/\n{3,}/g, "\n\n");
685
+ text = text.replaceAll(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
686
+ text = text.replaceAll(/<br\s*\/?>/gi, "\n");
687
+ text = text.replaceAll(/<\/?(p|div)>/gi, "\n");
688
+ text = text.replaceAll(/<li>/gi, "\n");
689
+ text = text.replaceAll(/<[^>]*>/g, "");
690
+ text = text.replaceAll("&amp;", "&");
691
+ text = text.replaceAll("&lt;", "<");
692
+ text = text.replaceAll("&gt;", ">");
693
+ text = text.replaceAll("&quot;", '"');
694
+ text = text.replaceAll("&#39;", "'");
695
+ text = text.replaceAll("&nbsp;", " ");
696
+ text = text.replaceAll(/\n{3,}/g, "\n\n");
212
697
  return text.trim();
213
698
  }
214
- function formatWorkItem(workItem, short) {
215
- const lines = [];
699
+ function convertRichText(html, markdown) {
700
+ if (!html) return "";
701
+ return markdown ? toMarkdown(html) : stripHtml(html);
702
+ }
703
+ function formatExtraFields(extraFields, markdown) {
704
+ return Object.entries(extraFields).map(([refName, value]) => {
705
+ const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
706
+ const displayValue = markdown ? toMarkdown(value) : value;
707
+ return `${fieldLabel.padEnd(13)}${displayValue}`;
708
+ });
709
+ }
710
+ function summarizeDescription(text, label) {
711
+ const descLines = text.split("\n").filter((l) => l.trim() !== "");
712
+ const firstThree = descLines.slice(0, 3);
713
+ const suffix = descLines.length > 3 ? "\n..." : "";
714
+ return [`${label("Description:")}${firstThree.join("\n")}${suffix}`];
715
+ }
716
+ function formatWorkItem(workItem, short, markdown = false) {
216
717
  const label = (name) => name.padEnd(13);
217
- lines.push(`${label("ID:")}${workItem.id}`);
218
- lines.push(`${label("Type:")}${workItem.type}`);
219
- lines.push(`${label("Title:")}${workItem.title}`);
220
- lines.push(`${label("State:")}${workItem.state}`);
221
- lines.push(`${label("Assigned To:")}${workItem.assignedTo ?? "Unassigned"}`);
718
+ const lines = [
719
+ `${label("ID:")}${workItem.id}`,
720
+ `${label("Type:")}${workItem.type}`,
721
+ `${label("Title:")}${workItem.title}`,
722
+ `${label("State:")}${workItem.state}`,
723
+ `${label("Assigned To:")}${workItem.assignedTo ?? "Unassigned"}`
724
+ ];
222
725
  if (!short) {
223
- lines.push(`${label("Area:")}${workItem.areaPath}`);
224
- lines.push(`${label("Iteration:")}${workItem.iterationPath}`);
726
+ lines.push(
727
+ `${label("Area:")}${workItem.areaPath}`,
728
+ `${label("Iteration:")}${workItem.iterationPath}`
729
+ );
225
730
  }
226
731
  lines.push(`${label("URL:")}${workItem.url}`);
732
+ if (workItem.extraFields) {
733
+ lines.push(...formatExtraFields(workItem.extraFields, markdown));
734
+ }
227
735
  lines.push("");
228
- const descriptionText = workItem.description ? stripHtml(workItem.description) : "";
736
+ const descriptionText = convertRichText(workItem.description, markdown);
229
737
  if (short) {
230
- const descLines = descriptionText.split("\n").filter((l) => l.trim() !== "");
231
- const firstThree = descLines.slice(0, 3);
232
- const truncated = descLines.length > 3;
233
- const descSummary = firstThree.join("\n") + (truncated ? "\n..." : "");
234
- lines.push(`${label("Description:")}${descSummary}`);
738
+ lines.push(...summarizeDescription(descriptionText, label));
235
739
  } else {
236
- lines.push("Description:");
237
- lines.push(descriptionText);
740
+ lines.push("Description:", descriptionText);
238
741
  }
239
742
  return lines.join("\n");
240
743
  }
241
744
  function createGetItemCommand() {
242
745
  const command = new Command("get-item");
243
- 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").action(
746
+ 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").option("--markdown", "convert rich text fields to markdown").action(
244
747
  async (idStr, options) => {
245
- const id = parseInt(idStr, 10);
246
- if (!Number.isInteger(id) || id <= 0) {
247
- process.stderr.write(
248
- `Error: Work item ID must be a positive integer. Got: "${idStr}"
249
- `
250
- );
251
- process.exit(1);
252
- }
253
- const hasOrg = options.org !== void 0;
254
- const hasProject = options.project !== void 0;
255
- if (hasOrg !== hasProject) {
256
- process.stderr.write(
257
- "Error: --org and --project must both be provided, or both omitted.\n"
258
- );
259
- process.exit(1);
260
- }
748
+ const id = parseWorkItemId(idStr);
749
+ validateOrgProjectPair(options);
261
750
  let context;
262
751
  try {
263
- if (options.org && options.project) {
264
- context = { org: options.org, project: options.project };
265
- } else {
266
- context = detectAzdoContext();
267
- }
752
+ context = resolveContext(options);
268
753
  const credential = await resolvePat();
269
- const workItem = await getWorkItem(context, id, credential.pat);
270
- const output = formatWorkItem(workItem, options.short ?? false);
754
+ const fieldsList = options.fields === void 0 ? parseRequestedFields(loadConfig().fields) : parseRequestedFields(options.fields);
755
+ const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
756
+ const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
757
+ const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
271
758
  process.stdout.write(output + "\n");
272
759
  } catch (err) {
273
- const error = err instanceof Error ? err : new Error(String(err));
274
- const msg = error.message;
275
- if (msg === "AUTH_FAILED") {
276
- process.stderr.write(
277
- 'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (read)" scope.\n'
278
- );
279
- } else if (msg === "NOT_FOUND") {
280
- process.stderr.write(
281
- `Error: Work item ${id} not found in ${context.org}/${context.project}.
282
- `
283
- );
284
- } else if (msg === "PERMISSION_DENIED") {
285
- process.stderr.write(
286
- `Error: Access denied. Your PAT may lack permissions for project "${context.project}".
287
- `
288
- );
289
- } else if (msg === "NETWORK_ERROR") {
290
- process.stderr.write(
291
- "Error: Could not connect to Azure DevOps. Check your network connection.\n"
292
- );
293
- } else if (msg.includes("Not in a git repository") || msg.includes("is not an Azure DevOps URL") || msg.includes("Authentication cancelled")) {
294
- process.stderr.write(`Error: ${msg}
295
- `);
296
- } else {
297
- process.stderr.write(`Error: ${msg}
298
- `);
299
- }
300
- process.exit(1);
760
+ handleCommandError(err, id, context, "read", false);
301
761
  }
302
762
  }
303
763
  );
@@ -319,11 +779,1121 @@ function createClearPatCommand() {
319
779
  return command;
320
780
  }
321
781
 
782
+ // src/commands/config.ts
783
+ import { Command as Command3 } from "commander";
784
+ import { createInterface as createInterface2 } from "readline";
785
+ function formatConfigValue(value, unsetFallback = "") {
786
+ if (value === void 0) {
787
+ return unsetFallback;
788
+ }
789
+ return Array.isArray(value) ? value.join(",") : value;
790
+ }
791
+ function writeConfigList(cfg) {
792
+ const keyWidth = 10;
793
+ const valueWidth = 30;
794
+ for (const setting of SETTINGS) {
795
+ const raw = cfg[setting.key];
796
+ const value = formatConfigValue(raw, "(not set)");
797
+ const marker = raw === void 0 && setting.required ? " *" : "";
798
+ process.stdout.write(
799
+ `${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
800
+ `
801
+ );
802
+ }
803
+ const hasUnset = SETTINGS.some((s) => s.required && cfg[s.key] === void 0);
804
+ if (hasUnset) {
805
+ process.stdout.write(
806
+ '\n* = required but not configured. Run "azdo config wizard" to set up.\n'
807
+ );
808
+ }
809
+ }
810
+ function createAsk(rl) {
811
+ return (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
812
+ }
813
+ async function promptForSetting(cfg, setting, ask) {
814
+ const currentDisplay = String(formatConfigValue(cfg[setting.key], ""));
815
+ const requiredTag = setting.required ? " (required)" : " (optional)";
816
+ process.stderr.write(`${setting.description}${requiredTag}
817
+ `);
818
+ if (setting.example) {
819
+ process.stderr.write(` Example: ${setting.example}
820
+ `);
821
+ }
822
+ const defaultHint = currentDisplay ? ` [${currentDisplay}]` : "";
823
+ const answer = await ask(` ${setting.key}${defaultHint}: `);
824
+ const trimmed = answer.trim();
825
+ if (trimmed) {
826
+ setConfigValue(setting.key, trimmed);
827
+ process.stderr.write(` -> Set "${setting.key}" to "${trimmed}"
828
+
829
+ `);
830
+ return;
831
+ }
832
+ if (currentDisplay) {
833
+ process.stderr.write(` -> Kept "${setting.key}" as "${currentDisplay}"
834
+
835
+ `);
836
+ return;
837
+ }
838
+ process.stderr.write(` -> Skipped "${setting.key}"
839
+
840
+ `);
841
+ }
842
+ function createConfigCommand() {
843
+ const config = new Command3("config");
844
+ config.description("Manage CLI settings");
845
+ const set = new Command3("set");
846
+ set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields)").argument("<value>", "setting value").option("--json", "output in JSON format").action((key, value, options) => {
847
+ try {
848
+ setConfigValue(key, value);
849
+ if (options.json) {
850
+ const output = { key, value };
851
+ if (key === "fields") {
852
+ output.value = value.split(",").map((s) => s.trim());
853
+ }
854
+ process.stdout.write(JSON.stringify(output) + "\n");
855
+ } else {
856
+ process.stdout.write(`Set "${key}" to "${value}"
857
+ `);
858
+ }
859
+ } catch (err) {
860
+ const message = err instanceof Error ? err.message : String(err);
861
+ process.stderr.write(`Error: ${message}
862
+ `);
863
+ process.exit(1);
864
+ }
865
+ });
866
+ const get = new Command3("get");
867
+ get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
868
+ try {
869
+ const value = getConfigValue(key);
870
+ if (options.json) {
871
+ process.stdout.write(
872
+ JSON.stringify({ key, value: value ?? null }) + "\n"
873
+ );
874
+ } else if (value === void 0) {
875
+ process.stdout.write(`Setting "${key}" is not configured.
876
+ `);
877
+ } else if (Array.isArray(value)) {
878
+ process.stdout.write(value.join(",") + "\n");
879
+ } else {
880
+ process.stdout.write(value + "\n");
881
+ }
882
+ } catch (err) {
883
+ const message = err instanceof Error ? err.message : String(err);
884
+ process.stderr.write(`Error: ${message}
885
+ `);
886
+ process.exit(1);
887
+ }
888
+ });
889
+ const list = new Command3("list");
890
+ list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
891
+ const cfg = loadConfig();
892
+ if (options.json) {
893
+ process.stdout.write(JSON.stringify(cfg) + "\n");
894
+ return;
895
+ }
896
+ writeConfigList(cfg);
897
+ });
898
+ const unset = new Command3("unset");
899
+ unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
900
+ try {
901
+ unsetConfigValue(key);
902
+ if (options.json) {
903
+ process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
904
+ } else {
905
+ process.stdout.write(`Unset "${key}"
906
+ `);
907
+ }
908
+ } catch (err) {
909
+ const message = err instanceof Error ? err.message : String(err);
910
+ process.stderr.write(`Error: ${message}
911
+ `);
912
+ process.exit(1);
913
+ }
914
+ });
915
+ const wizard = new Command3("wizard");
916
+ wizard.description("Interactive wizard to configure all settings").action(async () => {
917
+ if (!process.stdin.isTTY) {
918
+ process.stderr.write(
919
+ "Error: Wizard requires an interactive terminal.\n"
920
+ );
921
+ process.exit(1);
922
+ }
923
+ const cfg = loadConfig();
924
+ const rl = createInterface2({
925
+ input: process.stdin,
926
+ output: process.stderr
927
+ });
928
+ const ask = createAsk(rl);
929
+ process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
930
+ process.stderr.write("=======================================\n\n");
931
+ for (const setting of SETTINGS) {
932
+ await promptForSetting(cfg, setting, ask);
933
+ }
934
+ rl.close();
935
+ process.stderr.write("Configuration complete!\n");
936
+ });
937
+ config.addCommand(set);
938
+ config.addCommand(get);
939
+ config.addCommand(list);
940
+ config.addCommand(unset);
941
+ config.addCommand(wizard);
942
+ return config;
943
+ }
944
+
945
+ // src/commands/set-state.ts
946
+ import { Command as Command4 } from "commander";
947
+ function createSetStateCommand() {
948
+ const command = new Command4("set-state");
949
+ 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(
950
+ async (idStr, state, options) => {
951
+ const id = parseWorkItemId(idStr);
952
+ validateOrgProjectPair(options);
953
+ let context;
954
+ try {
955
+ context = resolveContext(options);
956
+ const credential = await resolvePat();
957
+ const operations = [
958
+ { op: "add", path: "/fields/System.State", value: state }
959
+ ];
960
+ const result = await updateWorkItem(context, id, credential.pat, "System.State", operations);
961
+ if (options.json) {
962
+ process.stdout.write(
963
+ JSON.stringify({
964
+ id: result.id,
965
+ rev: result.rev,
966
+ title: result.title,
967
+ field: result.fieldName,
968
+ value: result.fieldValue
969
+ }) + "\n"
970
+ );
971
+ } else {
972
+ process.stdout.write(`Updated work item ${result.id}: State -> ${state}
973
+ `);
974
+ }
975
+ } catch (err) {
976
+ handleCommandError(err, id, context, "write");
977
+ }
978
+ }
979
+ );
980
+ return command;
981
+ }
982
+
983
+ // src/commands/assign.ts
984
+ import { Command as Command5 } from "commander";
985
+ function createAssignCommand() {
986
+ const command = new Command5("assign");
987
+ 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(
988
+ async (idStr, name, options) => {
989
+ const id = parseWorkItemId(idStr);
990
+ if (!name && !options.unassign) {
991
+ process.stderr.write(
992
+ "Error: Either provide a user name or use --unassign.\n"
993
+ );
994
+ process.exit(1);
995
+ }
996
+ if (name && options.unassign) {
997
+ process.stderr.write(
998
+ "Error: Cannot provide both a user name and --unassign.\n"
999
+ );
1000
+ process.exit(1);
1001
+ }
1002
+ validateOrgProjectPair(options);
1003
+ let context;
1004
+ try {
1005
+ context = resolveContext(options);
1006
+ const credential = await resolvePat();
1007
+ const value = options.unassign ? "" : name;
1008
+ const operations = [
1009
+ { op: "add", path: "/fields/System.AssignedTo", value }
1010
+ ];
1011
+ const result = await updateWorkItem(context, id, credential.pat, "System.AssignedTo", operations);
1012
+ if (options.json) {
1013
+ process.stdout.write(
1014
+ JSON.stringify({
1015
+ id: result.id,
1016
+ rev: result.rev,
1017
+ title: result.title,
1018
+ field: result.fieldName,
1019
+ value: result.fieldValue
1020
+ }) + "\n"
1021
+ );
1022
+ } else {
1023
+ const displayValue = options.unassign ? "(unassigned)" : name;
1024
+ process.stdout.write(`Updated work item ${result.id}: Assigned To -> ${displayValue}
1025
+ `);
1026
+ }
1027
+ } catch (err) {
1028
+ handleCommandError(err, id, context, "write");
1029
+ }
1030
+ }
1031
+ );
1032
+ return command;
1033
+ }
1034
+
1035
+ // src/commands/set-field.ts
1036
+ import { Command as Command6 } from "commander";
1037
+ function createSetFieldCommand() {
1038
+ const command = new Command6("set-field");
1039
+ 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(
1040
+ async (idStr, field, value, options) => {
1041
+ const id = parseWorkItemId(idStr);
1042
+ validateOrgProjectPair(options);
1043
+ let context;
1044
+ try {
1045
+ context = resolveContext(options);
1046
+ const credential = await resolvePat();
1047
+ const operations = [
1048
+ { op: "add", path: `/fields/${field}`, value }
1049
+ ];
1050
+ const result = await updateWorkItem(context, id, credential.pat, field, operations);
1051
+ if (options.json) {
1052
+ process.stdout.write(
1053
+ JSON.stringify({
1054
+ id: result.id,
1055
+ rev: result.rev,
1056
+ title: result.title,
1057
+ field: result.fieldName,
1058
+ value: result.fieldValue
1059
+ }) + "\n"
1060
+ );
1061
+ } else {
1062
+ process.stdout.write(`Updated work item ${result.id}: ${field} -> ${value}
1063
+ `);
1064
+ }
1065
+ } catch (err) {
1066
+ handleCommandError(err, id, context, "write");
1067
+ }
1068
+ }
1069
+ );
1070
+ return command;
1071
+ }
1072
+
1073
+ // src/commands/get-md-field.ts
1074
+ import { Command as Command7 } from "commander";
1075
+ function createGetMdFieldCommand() {
1076
+ const command = new Command7("get-md-field");
1077
+ 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(
1078
+ async (idStr, field, options) => {
1079
+ const id = parseWorkItemId(idStr);
1080
+ validateOrgProjectPair(options);
1081
+ let context;
1082
+ try {
1083
+ context = resolveContext(options);
1084
+ const credential = await resolvePat();
1085
+ const value = await getWorkItemFieldValue(context, id, credential.pat, field);
1086
+ if (value === null) {
1087
+ process.stdout.write("\n");
1088
+ } else {
1089
+ process.stdout.write(toMarkdown(value) + "\n");
1090
+ }
1091
+ } catch (err) {
1092
+ handleCommandError(err, id, context, "read");
1093
+ }
1094
+ }
1095
+ );
1096
+ return command;
1097
+ }
1098
+
1099
+ // src/commands/set-md-field.ts
1100
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
1101
+ import { Command as Command8 } from "commander";
1102
+ function fail(message) {
1103
+ process.stderr.write(`Error: ${message}
1104
+ `);
1105
+ process.exit(1);
1106
+ }
1107
+ function resolveContent(inlineContent, options) {
1108
+ if (inlineContent && options.file) {
1109
+ fail("Cannot specify both inline content and --file.");
1110
+ }
1111
+ if (options.file) {
1112
+ return readFileContent(options.file);
1113
+ }
1114
+ if (inlineContent) {
1115
+ return inlineContent;
1116
+ }
1117
+ return null;
1118
+ }
1119
+ function readFileContent(filePath) {
1120
+ if (!existsSync(filePath)) {
1121
+ fail(`File not found: ${filePath}`);
1122
+ }
1123
+ try {
1124
+ return readFileSync2(filePath, "utf-8");
1125
+ } catch {
1126
+ fail(`Cannot read file: ${filePath}`);
1127
+ }
1128
+ }
1129
+ async function readStdinContent() {
1130
+ if (process.stdin.isTTY) {
1131
+ fail(
1132
+ "No content provided. Pass markdown content as the third argument, use --file, or pipe via stdin."
1133
+ );
1134
+ }
1135
+ const chunks = [];
1136
+ for await (const chunk of process.stdin) {
1137
+ chunks.push(chunk);
1138
+ }
1139
+ const stdinContent = Buffer.concat(chunks).toString("utf-8").trimEnd();
1140
+ if (!stdinContent) {
1141
+ fail(
1142
+ "No content provided via stdin. Pipe markdown content or use inline content or --file."
1143
+ );
1144
+ }
1145
+ return stdinContent;
1146
+ }
1147
+ function formatOutput(result, options, field) {
1148
+ if (options.json) {
1149
+ process.stdout.write(
1150
+ JSON.stringify({
1151
+ id: result.id,
1152
+ rev: result.rev,
1153
+ field: result.fieldName,
1154
+ value: result.fieldValue
1155
+ }) + "\n"
1156
+ );
1157
+ } else {
1158
+ process.stdout.write(`Updated work item ${result.id}: ${field} set with markdown content
1159
+ `);
1160
+ }
1161
+ }
1162
+ function createSetMdFieldCommand() {
1163
+ const command = new Command8("set-md-field");
1164
+ 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(
1165
+ async (idStr, field, inlineContent, options) => {
1166
+ const id = parseWorkItemId(idStr);
1167
+ validateOrgProjectPair(options);
1168
+ const content = resolveContent(inlineContent, options) ?? await readStdinContent();
1169
+ let context;
1170
+ try {
1171
+ context = resolveContext(options);
1172
+ const credential = await resolvePat();
1173
+ const operations = [
1174
+ { op: "add", path: `/fields/${field}`, value: content },
1175
+ { op: "add", path: `/multilineFieldsFormat/${field}`, value: "Markdown" }
1176
+ ];
1177
+ const result = await updateWorkItem(context, id, credential.pat, field, operations);
1178
+ formatOutput(result, options, field);
1179
+ } catch (err) {
1180
+ handleCommandError(err, id, context, "write");
1181
+ }
1182
+ }
1183
+ );
1184
+ return command;
1185
+ }
1186
+
1187
+ // src/commands/upsert.ts
1188
+ import { existsSync as existsSync2, readFileSync as readFileSync3, unlinkSync } from "fs";
1189
+ import { Command as Command9 } from "commander";
1190
+
1191
+ // src/services/task-document.ts
1192
+ var FIELD_ALIASES = /* @__PURE__ */ new Map([
1193
+ ["title", "System.Title"],
1194
+ ["assignedto", "System.AssignedTo"],
1195
+ ["assigned to", "System.AssignedTo"],
1196
+ ["state", "System.State"],
1197
+ ["description", "System.Description"],
1198
+ ["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
1199
+ ["acceptance criteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
1200
+ ["tags", "System.Tags"],
1201
+ ["priority", "Microsoft.VSTS.Common.Priority"]
1202
+ ]);
1203
+ var RICH_TEXT_FIELDS = /* @__PURE__ */ new Set([
1204
+ "System.Description",
1205
+ "Microsoft.VSTS.Common.AcceptanceCriteria"
1206
+ ]);
1207
+ var REFERENCE_NAME_PATTERN = /^[A-Z][A-Za-z0-9]*(\.[A-Za-z0-9]+)+$/;
1208
+ function normalizeAlias(name) {
1209
+ return name.trim().replaceAll(/\s+/g, " ").toLowerCase();
1210
+ }
1211
+ function parseScalarValue(rawValue, fieldName) {
1212
+ if (rawValue === void 0) {
1213
+ throw new Error(`Malformed YAML front matter: missing value for "${fieldName}"`);
1214
+ }
1215
+ const trimmed = rawValue.trim();
1216
+ if (trimmed === "" || trimmed === "null" || trimmed === "~") {
1217
+ return null;
1218
+ }
1219
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
1220
+ return trimmed.slice(1, -1);
1221
+ }
1222
+ if (/^[[{]|^[>|]-?$/.test(trimmed)) {
1223
+ throw new Error(`Malformed YAML front matter: unsupported value for "${fieldName}"`);
1224
+ }
1225
+ return trimmed;
1226
+ }
1227
+ function parseFrontMatter(content) {
1228
+ if (!content.startsWith("---")) {
1229
+ return { frontMatter: "", remainder: content };
1230
+ }
1231
+ const frontMatterPattern = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
1232
+ const match = frontMatterPattern.exec(content);
1233
+ if (!match) {
1234
+ throw new Error('Malformed YAML front matter: missing closing "---"');
1235
+ }
1236
+ return {
1237
+ frontMatter: match[1],
1238
+ remainder: content.slice(match[0].length)
1239
+ };
1240
+ }
1241
+ function assertKnownField(name, kind) {
1242
+ const resolved = resolveFieldName(name);
1243
+ if (!resolved) {
1244
+ const prefix = kind === "rich-text" ? "Unknown rich-text field" : "Unknown field";
1245
+ throw new Error(`${prefix}: ${name}`);
1246
+ }
1247
+ if (kind === "rich-text" && !RICH_TEXT_FIELDS.has(resolved)) {
1248
+ throw new Error(`Unknown rich-text field: ${name}`);
1249
+ }
1250
+ return resolved;
1251
+ }
1252
+ function pushField(fields, seen, refName, value, kind) {
1253
+ if (seen.has(refName)) {
1254
+ throw new Error(`Duplicate field: ${refName}`);
1255
+ }
1256
+ seen.add(refName);
1257
+ fields.push({
1258
+ refName,
1259
+ value,
1260
+ op: value === null ? "clear" : "set",
1261
+ kind
1262
+ });
1263
+ }
1264
+ function parseScalarFields(frontMatter, fields, seen) {
1265
+ if (frontMatter.trim() === "") {
1266
+ return;
1267
+ }
1268
+ for (const rawLine of frontMatter.split(/\r?\n/)) {
1269
+ const line = rawLine.trim();
1270
+ if (line === "") {
1271
+ continue;
1272
+ }
1273
+ const separatorIndex = rawLine.indexOf(":");
1274
+ if (separatorIndex <= 0) {
1275
+ throw new Error(`Malformed YAML front matter: ${rawLine.trim()}`);
1276
+ }
1277
+ const rawName = rawLine.slice(0, separatorIndex).trim();
1278
+ const rawValue = rawLine.slice(separatorIndex + 1);
1279
+ const refName = assertKnownField(rawName, "scalar");
1280
+ const value = parseScalarValue(rawValue, rawName);
1281
+ pushField(fields, seen, refName, value, "scalar");
1282
+ }
1283
+ }
1284
+ function parseRichTextSections(content, fields, seen) {
1285
+ const normalizedContent = content.replaceAll("\r\n", "\n");
1286
+ const lines = normalizedContent.split("\n");
1287
+ const headings = [];
1288
+ for (let index = 0; index < lines.length; index += 1) {
1289
+ const line = lines[index];
1290
+ if (!line.startsWith("##")) {
1291
+ continue;
1292
+ }
1293
+ const headingBody = line.slice(2);
1294
+ if (headingBody.trim() === "" || !headingBody.startsWith(" ") && !headingBody.startsWith(" ")) {
1295
+ continue;
1296
+ }
1297
+ headings.push({
1298
+ lineIndex: index,
1299
+ rawName: headingBody.trim()
1300
+ });
1301
+ }
1302
+ if (headings.length === 0) {
1303
+ return;
1304
+ }
1305
+ for (let index = 0; index < headings[0].lineIndex; index += 1) {
1306
+ if (lines[index].trim() !== "") {
1307
+ throw new Error("Unexpected content before the first markdown heading section");
1308
+ }
1309
+ }
1310
+ for (let index = 0; index < headings.length; index += 1) {
1311
+ const { lineIndex, rawName } = headings[index];
1312
+ const refName = assertKnownField(rawName, "rich-text");
1313
+ const bodyStart = lineIndex + 1;
1314
+ const bodyEnd = index + 1 < headings.length ? headings[index + 1].lineIndex : lines.length;
1315
+ const rawBody = lines.slice(bodyStart, bodyEnd).join("\n");
1316
+ const value = rawBody.trim() === "" ? null : rawBody.trimEnd();
1317
+ pushField(fields, seen, refName, value, "rich-text");
1318
+ }
1319
+ }
1320
+ function resolveFieldName(name) {
1321
+ const trimmed = name.trim();
1322
+ if (trimmed === "") {
1323
+ return null;
1324
+ }
1325
+ const alias = FIELD_ALIASES.get(normalizeAlias(trimmed));
1326
+ if (alias) {
1327
+ return alias;
1328
+ }
1329
+ return REFERENCE_NAME_PATTERN.test(trimmed) ? trimmed : null;
1330
+ }
1331
+ function parseTaskDocument(content) {
1332
+ const { frontMatter, remainder } = parseFrontMatter(content);
1333
+ const fields = [];
1334
+ const seen = /* @__PURE__ */ new Set();
1335
+ parseScalarFields(frontMatter, fields, seen);
1336
+ parseRichTextSections(remainder, fields, seen);
1337
+ return { fields };
1338
+ }
1339
+
1340
+ // src/commands/upsert.ts
1341
+ function fail2(message) {
1342
+ process.stderr.write(`Error: ${message}
1343
+ `);
1344
+ process.exit(1);
1345
+ }
1346
+ function loadSourceContent(options) {
1347
+ validateSource(options);
1348
+ if (options.content !== void 0) {
1349
+ return { content: options.content };
1350
+ }
1351
+ const filePath = options.file;
1352
+ if (!existsSync2(filePath)) {
1353
+ fail2(`File not found: ${filePath}`);
1354
+ }
1355
+ try {
1356
+ return {
1357
+ content: readFileSync3(filePath, "utf-8"),
1358
+ sourceFile: filePath
1359
+ };
1360
+ } catch {
1361
+ fail2(`Cannot read file: ${filePath}`);
1362
+ }
1363
+ }
1364
+ function toPatchOperations(fields, action) {
1365
+ const operations = [];
1366
+ for (const field of fields) {
1367
+ if (field.op === "clear") {
1368
+ if (action === "updated") {
1369
+ operations.push({ op: "remove", path: `/fields/${field.refName}` });
1370
+ }
1371
+ continue;
1372
+ }
1373
+ operations.push({ op: "add", path: `/fields/${field.refName}`, value: field.value ?? "" });
1374
+ if (field.kind === "rich-text") {
1375
+ operations.push({
1376
+ op: "add",
1377
+ path: `/multilineFieldsFormat/${field.refName}`,
1378
+ value: "Markdown"
1379
+ });
1380
+ }
1381
+ }
1382
+ return operations;
1383
+ }
1384
+ function buildAppliedFields(fields) {
1385
+ const applied = {};
1386
+ for (const field of fields) {
1387
+ applied[field.refName] = field.value;
1388
+ }
1389
+ return applied;
1390
+ }
1391
+ function ensureTitleForCreate(fields) {
1392
+ const titleField = fields.find((field) => field.refName === "System.Title");
1393
+ if (!titleField || titleField.op === "clear" || titleField.value === null || titleField.value.trim() === "") {
1394
+ fail2("Title is required when creating a work item.");
1395
+ }
1396
+ }
1397
+ function writeSuccess(result, options) {
1398
+ if (options.json) {
1399
+ process.stdout.write(`${JSON.stringify(result)}
1400
+ `);
1401
+ return;
1402
+ }
1403
+ const verb = result.action === "created" ? "Created" : "Updated";
1404
+ const fields = Object.keys(result.fields).join(", ");
1405
+ const suffix = fields ? ` (${fields})` : "";
1406
+ process.stdout.write(`${verb} ${result.workItemType} #${result.id}${suffix}
1407
+ `);
1408
+ }
1409
+ function cleanupSourceFile(sourceFile) {
1410
+ if (!sourceFile) {
1411
+ return;
1412
+ }
1413
+ try {
1414
+ unlinkSync(sourceFile);
1415
+ } catch {
1416
+ process.stderr.write(`Warning: upsert succeeded but could not delete source file: ${sourceFile}
1417
+ `);
1418
+ }
1419
+ }
1420
+ function resolveCreateType(id, options) {
1421
+ if (options.type === void 0) {
1422
+ return "Task";
1423
+ }
1424
+ if (id !== void 0) {
1425
+ fail2("--type can only be used when creating a work item.");
1426
+ }
1427
+ const trimmedType = options.type.trim();
1428
+ if (trimmedType === "") {
1429
+ fail2("--type must be a non-empty work item type.");
1430
+ }
1431
+ return trimmedType;
1432
+ }
1433
+ function buildUpsertResult(action, writeResult, fields, fallbackWorkItemType) {
1434
+ const appliedFields = buildAppliedFields(fields);
1435
+ const workItemType = writeResult.fields["System.WorkItemType"];
1436
+ return {
1437
+ action,
1438
+ id: writeResult.id,
1439
+ workItemType: typeof workItemType === "string" && workItemType.trim() !== "" ? workItemType : fallbackWorkItemType,
1440
+ fields: appliedFields
1441
+ };
1442
+ }
1443
+ function isUpdateWriteError(err) {
1444
+ return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message.startsWith("NOT_FOUND") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("UPDATE_REJECTED:");
1445
+ }
1446
+ function isCreateWriteError(err) {
1447
+ return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
1448
+ }
1449
+ function handleUpsertError(err, id, context) {
1450
+ if (!(err instanceof Error)) {
1451
+ process.stderr.write(`Error: ${String(err)}
1452
+ `);
1453
+ process.exit(1);
1454
+ }
1455
+ if (id === void 0 && err.message.startsWith("CREATE_REJECTED:")) {
1456
+ process.stderr.write(`Error: ${formatCreateError(err)}
1457
+ `);
1458
+ process.exit(1);
1459
+ }
1460
+ if (id !== void 0 && isUpdateWriteError(err)) {
1461
+ handleCommandError(err, id, context, "write");
1462
+ return;
1463
+ }
1464
+ if (id === void 0 && isCreateWriteError(err)) {
1465
+ handleCommandError(err, 0, context, "write");
1466
+ return;
1467
+ }
1468
+ process.stderr.write(`Error: ${err.message}
1469
+ `);
1470
+ process.exit(1);
1471
+ }
1472
+ function createUpsertCommand() {
1473
+ const command = new Command9("upsert");
1474
+ command.description("Create or update a work item from a markdown document").argument("[id]", "work item ID to update; omit to create a new work item").option("--content <markdown>", "task document content").option("--file <path>", "read task document from file").option("--type <workItemType>", "create mode work item type (defaults to Task)").option("--json", "output result as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(async (idStr, options) => {
1475
+ validateOrgProjectPair(options);
1476
+ const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
1477
+ const { content, sourceFile } = loadSourceContent(options);
1478
+ const createType = resolveCreateType(id, options);
1479
+ let context;
1480
+ try {
1481
+ context = resolveContext(options);
1482
+ const document = parseTaskDocument(content);
1483
+ const action = id === void 0 ? "created" : "updated";
1484
+ if (action === "created") {
1485
+ ensureTitleForCreate(document.fields);
1486
+ }
1487
+ const operations = toPatchOperations(document.fields, action);
1488
+ const credential = await resolvePat();
1489
+ let writeResult;
1490
+ if (action === "created") {
1491
+ writeResult = await createWorkItem(context, createType, credential.pat, operations);
1492
+ } else {
1493
+ if (id === void 0) {
1494
+ fail2("Work item ID is required for updates.");
1495
+ }
1496
+ writeResult = await applyWorkItemPatch(context, id, credential.pat, operations);
1497
+ }
1498
+ const result = buildUpsertResult(
1499
+ action,
1500
+ writeResult,
1501
+ document.fields,
1502
+ action === "created" ? createType : "Work item"
1503
+ );
1504
+ writeSuccess(result, options);
1505
+ cleanupSourceFile(sourceFile);
1506
+ } catch (err) {
1507
+ handleUpsertError(err, id, context);
1508
+ }
1509
+ });
1510
+ return command;
1511
+ }
1512
+
1513
+ // src/commands/list-fields.ts
1514
+ import { Command as Command10 } from "commander";
1515
+ function stringifyValue(value) {
1516
+ if (value === null || value === void 0) return "";
1517
+ if (typeof value === "object") return JSON.stringify(value);
1518
+ return String(value);
1519
+ }
1520
+ function formatRichValue(raw) {
1521
+ const md = htmlToMarkdown(raw);
1522
+ const lines = md.split("\n").filter((l) => l.trim() !== "");
1523
+ const preview = lines.slice(0, 5);
1524
+ const suffix = lines.length > 5 ? `
1525
+ \u2026 (${lines.length - 5} more lines)` : "";
1526
+ return preview.join("\n ") + suffix;
1527
+ }
1528
+ function formatFieldList(fields) {
1529
+ const entries = Object.entries(fields).sort(([a], [b]) => a.localeCompare(b));
1530
+ const maxKeyLen = Math.min(
1531
+ Math.max(...entries.map(([k]) => k.length)),
1532
+ 50
1533
+ );
1534
+ return entries.map(([key, value]) => {
1535
+ const raw = stringifyValue(value);
1536
+ if (raw === "") return `${key.padEnd(maxKeyLen + 2)}(empty)`;
1537
+ if (typeof value === "string" && isHtml(value)) {
1538
+ const preview = formatRichValue(value);
1539
+ return `${key.padEnd(maxKeyLen + 2)}[rich text]
1540
+ ${preview}`;
1541
+ }
1542
+ return `${key.padEnd(maxKeyLen + 2)}${raw}`;
1543
+ }).join("\n");
1544
+ }
1545
+ function createListFieldsCommand() {
1546
+ const command = new Command10("list-fields");
1547
+ command.description("List all fields of an Azure DevOps work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
1548
+ async (idStr, options) => {
1549
+ const id = parseWorkItemId(idStr);
1550
+ validateOrgProjectPair(options);
1551
+ let context;
1552
+ try {
1553
+ context = resolveContext(options);
1554
+ const credential = await resolvePat();
1555
+ const fields = await getWorkItemFields(context, id, credential.pat);
1556
+ if (options.json) {
1557
+ process.stdout.write(JSON.stringify({ id, fields }, null, 2) + "\n");
1558
+ } else {
1559
+ process.stdout.write(`Work Item ${id} \u2014 ${Object.keys(fields).length} fields
1560
+
1561
+ `);
1562
+ process.stdout.write(formatFieldList(fields) + "\n");
1563
+ }
1564
+ } catch (err) {
1565
+ handleCommandError(err, id, context, "read");
1566
+ }
1567
+ }
1568
+ );
1569
+ return command;
1570
+ }
1571
+
1572
+ // src/commands/pr.ts
1573
+ import { Command as Command11 } from "commander";
1574
+
1575
+ // src/services/pr-client.ts
1576
+ function buildPullRequestsUrl(context, repo, sourceBranch, opts) {
1577
+ const url = new URL(
1578
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
1579
+ );
1580
+ url.searchParams.set("api-version", "7.1");
1581
+ url.searchParams.set("searchCriteria.sourceRefName", `refs/heads/${sourceBranch}`);
1582
+ if (opts?.status) {
1583
+ url.searchParams.set("searchCriteria.status", opts.status);
1584
+ }
1585
+ if (opts?.targetBranch) {
1586
+ url.searchParams.set("searchCriteria.targetRefName", `refs/heads/${opts.targetBranch}`);
1587
+ }
1588
+ return url;
1589
+ }
1590
+ function mapPullRequest(repo, pullRequest) {
1591
+ return {
1592
+ id: pullRequest.pullRequestId,
1593
+ title: pullRequest.title,
1594
+ repository: repo,
1595
+ sourceRefName: pullRequest.sourceRefName,
1596
+ targetRefName: pullRequest.targetRefName,
1597
+ status: pullRequest.status,
1598
+ createdBy: pullRequest.createdBy?.displayName ?? null,
1599
+ url: pullRequest._links.web.href
1600
+ };
1601
+ }
1602
+ function mapComment(comment) {
1603
+ const content = comment.content?.trim();
1604
+ if (comment.isDeleted || !content) {
1605
+ return null;
1606
+ }
1607
+ return {
1608
+ id: comment.id,
1609
+ author: comment.author?.displayName ?? null,
1610
+ content,
1611
+ publishedAt: comment.publishedDate ?? null
1612
+ };
1613
+ }
1614
+ function mapThread(thread) {
1615
+ if (thread.status !== "active" && thread.status !== "pending") {
1616
+ return null;
1617
+ }
1618
+ const comments = thread.comments.map(mapComment).filter((comment) => comment !== null);
1619
+ if (comments.length === 0) {
1620
+ return null;
1621
+ }
1622
+ return {
1623
+ id: thread.id,
1624
+ status: thread.status,
1625
+ threadContext: thread.threadContext?.filePath ?? null,
1626
+ comments
1627
+ };
1628
+ }
1629
+ async function readJsonResponse(response) {
1630
+ if (!response.ok) {
1631
+ throw new Error(`HTTP_${response.status}`);
1632
+ }
1633
+ return response.json();
1634
+ }
1635
+ async function listPullRequests(context, repo, pat, sourceBranch, opts) {
1636
+ const response = await fetchWithErrors(
1637
+ buildPullRequestsUrl(context, repo, sourceBranch, opts).toString(),
1638
+ { headers: authHeaders(pat) }
1639
+ );
1640
+ const data = await readJsonResponse(response);
1641
+ return data.value.map((pullRequest) => mapPullRequest(repo, pullRequest));
1642
+ }
1643
+ async function openPullRequest(context, repo, pat, sourceBranch, title, description) {
1644
+ const existing = await listPullRequests(context, repo, pat, sourceBranch, {
1645
+ status: "active",
1646
+ targetBranch: "develop"
1647
+ });
1648
+ if (existing.length === 1) {
1649
+ return {
1650
+ branch: sourceBranch,
1651
+ targetBranch: "develop",
1652
+ created: false,
1653
+ pullRequest: existing[0]
1654
+ };
1655
+ }
1656
+ if (existing.length > 1) {
1657
+ throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest) => pullRequest.id).join(",")}`);
1658
+ }
1659
+ const payload = {
1660
+ sourceRefName: `refs/heads/${sourceBranch}`,
1661
+ targetRefName: "refs/heads/develop",
1662
+ title,
1663
+ description
1664
+ };
1665
+ const url = new URL(
1666
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
1667
+ );
1668
+ url.searchParams.set("api-version", "7.1");
1669
+ const response = await fetchWithErrors(url.toString(), {
1670
+ method: "POST",
1671
+ headers: {
1672
+ ...authHeaders(pat),
1673
+ "Content-Type": "application/json"
1674
+ },
1675
+ body: JSON.stringify(payload)
1676
+ });
1677
+ const data = await readJsonResponse(response);
1678
+ return {
1679
+ branch: sourceBranch,
1680
+ targetBranch: "develop",
1681
+ created: true,
1682
+ pullRequest: mapPullRequest(repo, data)
1683
+ };
1684
+ }
1685
+ async function getPullRequestThreads(context, repo, pat, prId) {
1686
+ const url = new URL(
1687
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
1688
+ );
1689
+ url.searchParams.set("api-version", "7.1");
1690
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
1691
+ const data = await readJsonResponse(response);
1692
+ return data.value.map(mapThread).filter((thread) => thread !== null);
1693
+ }
1694
+
1695
+ // src/commands/pr.ts
1696
+ function formatBranchName(refName) {
1697
+ return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
1698
+ }
1699
+ function writeError(message) {
1700
+ process.stderr.write(`Error: ${message}
1701
+ `);
1702
+ process.exit(1);
1703
+ }
1704
+ function handlePrCommandError(err, context, mode = "read") {
1705
+ const error = err instanceof Error ? err : new Error(String(err));
1706
+ if (error.message === "AUTH_FAILED") {
1707
+ const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
1708
+ writeError(`Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.`);
1709
+ }
1710
+ if (error.message === "PERMISSION_DENIED") {
1711
+ writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`);
1712
+ }
1713
+ if (error.message === "NETWORK_ERROR") {
1714
+ writeError("Could not connect to Azure DevOps. Check your network connection.");
1715
+ }
1716
+ if (error.message.startsWith("NOT_FOUND")) {
1717
+ writeError(`Azure DevOps repository not found in ${context?.org}/${context?.project}.`);
1718
+ }
1719
+ if (error.message.startsWith("HTTP_")) {
1720
+ writeError(`Azure DevOps request failed with ${error.message}.`);
1721
+ }
1722
+ writeError(error.message);
1723
+ }
1724
+ function formatPullRequestBlock(pullRequest) {
1725
+ return [
1726
+ `#${pullRequest.id} [${pullRequest.status}] ${pullRequest.title}`,
1727
+ `${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
1728
+ pullRequest.url
1729
+ ].join("\n");
1730
+ }
1731
+ function formatThreads(prId, title, threads) {
1732
+ const lines = [`Active comments for pull request #${prId}: ${title}`];
1733
+ for (const thread of threads) {
1734
+ lines.push("", `Thread #${thread.id} [${thread.status}] ${thread.threadContext ?? "(general)"}`);
1735
+ for (const comment of thread.comments) {
1736
+ lines.push(` ${comment.author ?? "Unknown"}: ${comment.content}`);
1737
+ }
1738
+ }
1739
+ return lines.join("\n");
1740
+ }
1741
+ async function resolvePrCommandContext(options) {
1742
+ const context = resolveContext(options);
1743
+ const repo = detectRepoName();
1744
+ const branch = getCurrentBranch();
1745
+ const credential = await resolvePat();
1746
+ return {
1747
+ context,
1748
+ repo,
1749
+ branch,
1750
+ pat: credential.pat
1751
+ };
1752
+ }
1753
+ function createPrStatusCommand() {
1754
+ const command = new Command11("status");
1755
+ command.description("Check pull requests for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
1756
+ validateOrgProjectPair(options);
1757
+ let context;
1758
+ try {
1759
+ const resolved = await resolvePrCommandContext(options);
1760
+ context = resolved.context;
1761
+ const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch);
1762
+ const { branch, repo } = resolved;
1763
+ const result = { branch, repository: repo, pullRequests };
1764
+ if (options.json) {
1765
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1766
+ `);
1767
+ return;
1768
+ }
1769
+ if (pullRequests.length === 0) {
1770
+ process.stdout.write(`No pull requests found for branch ${branch}.
1771
+ `);
1772
+ return;
1773
+ }
1774
+ process.stdout.write(`${pullRequests.map(formatPullRequestBlock).join("\n\n")}
1775
+ `);
1776
+ } catch (err) {
1777
+ handlePrCommandError(err, context, "read");
1778
+ }
1779
+ });
1780
+ return command;
1781
+ }
1782
+ function createPrOpenCommand() {
1783
+ const command = new Command11("open");
1784
+ command.description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option("--description <description>", "pull request description").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
1785
+ validateOrgProjectPair(options);
1786
+ const title = options.title?.trim();
1787
+ if (!title) {
1788
+ writeError("--title is required for pull request creation.");
1789
+ }
1790
+ const description = options.description?.trim();
1791
+ if (!description) {
1792
+ writeError("--description is required for pull request creation.");
1793
+ }
1794
+ let context;
1795
+ try {
1796
+ const resolved = await resolvePrCommandContext(options);
1797
+ context = resolved.context;
1798
+ if (resolved.branch === "develop") {
1799
+ writeError("Pull request creation requires a source branch other than develop.");
1800
+ }
1801
+ const result = await openPullRequest(
1802
+ resolved.context,
1803
+ resolved.repo,
1804
+ resolved.pat,
1805
+ resolved.branch,
1806
+ title,
1807
+ description
1808
+ );
1809
+ if (options.json) {
1810
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1811
+ `);
1812
+ return;
1813
+ }
1814
+ if (result.created) {
1815
+ process.stdout.write(`Created pull request #${result.pullRequest.id}: ${result.pullRequest.title}
1816
+ ${result.pullRequest.url}
1817
+ `);
1818
+ return;
1819
+ }
1820
+ process.stdout.write(
1821
+ `Active pull request already exists for ${resolved.branch} -> develop: #${result.pullRequest.id}
1822
+ ${result.pullRequest.url}
1823
+ `
1824
+ );
1825
+ } catch (err) {
1826
+ if (err instanceof Error && err.message.startsWith("AMBIGUOUS_PRS:")) {
1827
+ const ids = err.message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
1828
+ writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
1829
+ }
1830
+ handlePrCommandError(err, context, "write");
1831
+ }
1832
+ });
1833
+ return command;
1834
+ }
1835
+ function createPrCommentsCommand() {
1836
+ const command = new Command11("comments");
1837
+ command.description("List active pull request comments for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
1838
+ validateOrgProjectPair(options);
1839
+ let context;
1840
+ try {
1841
+ const resolved = await resolvePrCommandContext(options);
1842
+ context = resolved.context;
1843
+ const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch, {
1844
+ status: "active"
1845
+ });
1846
+ if (pullRequests.length === 0) {
1847
+ writeError(`No active pull request found for branch ${resolved.branch}.`);
1848
+ }
1849
+ if (pullRequests.length > 1) {
1850
+ const ids = pullRequests.map((pullRequest2) => `#${pullRequest2.id}`).join(", ");
1851
+ writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
1852
+ }
1853
+ const pullRequest = pullRequests[0];
1854
+ const threads = await getPullRequestThreads(resolved.context, resolved.repo, resolved.pat, pullRequest.id);
1855
+ const result = { branch: resolved.branch, pullRequest, threads };
1856
+ if (options.json) {
1857
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1858
+ `);
1859
+ return;
1860
+ }
1861
+ if (threads.length === 0) {
1862
+ process.stdout.write(`Pull request #${pullRequest.id} has no active comments.
1863
+ `);
1864
+ return;
1865
+ }
1866
+ process.stdout.write(`${formatThreads(pullRequest.id, pullRequest.title, threads)}
1867
+ `);
1868
+ } catch (err) {
1869
+ handlePrCommandError(err, context, "read");
1870
+ }
1871
+ });
1872
+ return command;
1873
+ }
1874
+ function createPrCommand() {
1875
+ const command = new Command11("pr");
1876
+ command.description("Manage Azure DevOps pull requests");
1877
+ command.addCommand(createPrStatusCommand());
1878
+ command.addCommand(createPrOpenCommand());
1879
+ command.addCommand(createPrCommentsCommand());
1880
+ return command;
1881
+ }
1882
+
322
1883
  // src/index.ts
323
- var program = new Command3();
1884
+ var program = new Command12();
324
1885
  program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
325
1886
  program.addCommand(createGetItemCommand());
326
1887
  program.addCommand(createClearPatCommand());
1888
+ program.addCommand(createConfigCommand());
1889
+ program.addCommand(createSetStateCommand());
1890
+ program.addCommand(createAssignCommand());
1891
+ program.addCommand(createSetFieldCommand());
1892
+ program.addCommand(createGetMdFieldCommand());
1893
+ program.addCommand(createSetMdFieldCommand());
1894
+ program.addCommand(createUpsertCommand());
1895
+ program.addCommand(createListFieldsCommand());
1896
+ program.addCommand(createPrCommand());
327
1897
  program.showHelpAfterError();
328
1898
  program.parse();
329
1899
  if (process.argv.length <= 2) {