azdo-cli 0.2.0-feature-macsecret.95 → 0.2.0-feature-integrationtest.109

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 +66 -0
  2. package/dist/index.js +440 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +15,9 @@ Azure DevOps CLI focused on work item read/write workflows.
15
15
  - Create or update Tasks from markdown documents (`upsert`)
16
16
  - Read rich-text fields as markdown (`get-md-field`)
17
17
  - Set rich-text fields as markdown from inline text, file, or stdin (`set-md-field`)
18
+ - Check branch pull request status, open PRs to `develop`, and review active comments (`pr`)
18
19
  - Persist org/project/default fields in local config (`config`)
20
+ - List all fields of a work item (`list-fields`)
19
21
  - Store PAT in OS credential store (or use `AZDO_PAT`)
20
22
 
21
23
  ## Installation
@@ -24,6 +26,20 @@ Azure DevOps CLI focused on work item read/write workflows.
24
26
  npm install -g azdo-cli
25
27
  ```
26
28
 
29
+ ## Utility Scripts
30
+
31
+ The repository also includes a helper script for syncing local `.env` entries into GitHub Actions secrets for the current repository:
32
+
33
+ ```bash
34
+ ./scripts/sync-env-to-gh-secrets.zsh
35
+ ```
36
+
37
+ It walks upward from the current directory until it finds a `.env`, then sets each valid `KEY=VALUE` entry with `gh secret set`. You can also limit the sync to selected keys:
38
+
39
+ ```bash
40
+ ./scripts/sync-env-to-gh-secrets.zsh FOO BAR
41
+ ```
42
+
27
43
  ## Authentication and Context Resolution
28
44
 
29
45
  PAT resolution order:
@@ -64,6 +80,8 @@ azdo upsert --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
64
80
  | `azdo upsert [id]` | Create or update a Task from markdown | `--content`, `--file`, `--json`, `--org`, `--project` |
65
81
  | `azdo get-md-field <id> <field>` | Get field as markdown | `--org`, `--project` |
66
82
  | `azdo set-md-field <id> <field> [content]` | Set markdown field | `--file`, `--json`, `--org`, `--project` |
83
+ | `azdo list-fields <id>` | List all fields of a work item | `--json`, `--org`, `--project` |
84
+ | `azdo pr <subcommand>` | Manage pull requests for the current branch | `status`, `open`, `comments`, `--json`, `--org`, `--project` |
67
85
  | `azdo config <subcommand>` | Manage saved settings | `set`, `get`, `list`, `unset`, `wizard`, `--json` |
68
86
  | `azdo clear-pat` | Remove stored PAT | none |
69
87
 
@@ -100,6 +118,16 @@ azdo assign 12345 --unassign
100
118
  azdo set-field 12345 System.Title "Updated title"
101
119
  ```
102
120
 
121
+ ### List Fields
122
+
123
+ ```bash
124
+ # List all fields with values (rich text fields preview first 5 lines)
125
+ azdo list-fields 12345
126
+
127
+ # JSON output
128
+ azdo list-fields 12345 --json
129
+ ```
130
+
103
131
  ### Markdown Display
104
132
 
105
133
  The `get-item` command can convert HTML rich-text fields to readable markdown. Resolution order:
@@ -124,6 +152,43 @@ azdo set-md-field 12345 System.Description --file ./description.md
124
152
  cat description.md | azdo set-md-field 12345 System.Description
125
153
  ```
126
154
 
155
+ ### Pull Request Commands
156
+
157
+ The `pr` command group uses the current git branch and the Azure DevOps `origin` remote automatically. It requires a PAT with `Code (Read)` scope for read operations and `Code (Read & Write)` for pull request creation.
158
+
159
+ ```bash
160
+ # Check whether the current branch already has pull requests
161
+ azdo pr status
162
+
163
+ # Open a pull request to develop
164
+ azdo pr open --title "Add PR handling" --description "Implements pr status, pr open, pr comments commands"
165
+
166
+ # Review active comments for the current branch's active pull request
167
+ azdo pr comments
168
+ ```
169
+
170
+ `azdo pr status`
171
+
172
+ - Lists all pull requests for the current branch, including active, completed, and abandoned PRs
173
+ - Prints `No pull requests found for branch <branch>.` when no PRs exist
174
+ - Supports `--json` for machine-readable output
175
+
176
+ `azdo pr open`
177
+
178
+ - Requires both `--title <title>` and `--description <description>`
179
+ - Targets `develop` automatically
180
+ - Creates a new active pull request when none exists
181
+ - Reuses the existing active PR when one already matches the branch and target
182
+ - Fails with a clear error when run from `develop` or when multiple active PRs already exist
183
+
184
+ `azdo pr comments`
185
+
186
+ - Resolves the single active pull request for the current branch
187
+ - Returns only active or pending threads with visible, non-deleted comments
188
+ - Groups text output by thread and shows file context when available
189
+ - Prints `Pull request #<id> has no active comments.` when the PR has no active comment threads
190
+ - Fails instead of guessing when no active PR or multiple active PRs exist
191
+
127
192
  ## azdo upsert
128
193
 
129
194
  `azdo upsert` accepts a single markdown task document and either creates a new Azure DevOps Task or updates an existing one. Omit `[id]` to create; pass `[id]` to update that work item in place.
@@ -232,6 +297,7 @@ azdo clear-pat
232
297
  ## JSON Output
233
298
 
234
299
  These commands support `--json` for machine-readable output:
300
+ - `list-fields`
235
301
  - `set-state`
236
302
  - `assign`
237
303
  - `set-field`
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 Command10 } from "commander";
4
+ import { Command as Command12 } from "commander";
5
5
 
6
6
  // src/version.ts
7
7
  import { readFileSync } from "fs";
@@ -64,9 +64,20 @@ function stringifyFieldValue(value) {
64
64
  function buildExtraFields(fields, requested) {
65
65
  const result = {};
66
66
  for (const name of requested) {
67
- const val = fields[name];
67
+ let val = fields[name];
68
+ let resolvedName = name;
69
+ if (val === void 0) {
70
+ const nameSuffix = name.split(".").pop().toLowerCase();
71
+ const match = Object.keys(fields).find(
72
+ (k) => k.split(".").pop().toLowerCase() === nameSuffix
73
+ );
74
+ if (match !== void 0) {
75
+ val = fields[match];
76
+ resolvedName = match;
77
+ }
78
+ }
68
79
  if (val !== void 0 && val !== null) {
69
- result[name] = stringifyFieldValue(val);
80
+ result[resolvedName] = stringifyFieldValue(val);
70
81
  }
71
82
  }
72
83
  return Object.keys(result).length > 0 ? result : null;
@@ -92,6 +103,25 @@ async function readWriteResponse(response, errorCode) {
92
103
  fields: data.fields
93
104
  };
94
105
  }
106
+ async function getWorkItemFields(context, id, pat) {
107
+ const url = new URL(
108
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
109
+ );
110
+ url.searchParams.set("api-version", "7.1");
111
+ url.searchParams.set("$expand", "all");
112
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
113
+ if (response.status === 400) {
114
+ const serverMessage = await readResponseMessage(response);
115
+ if (serverMessage) {
116
+ throw new Error(`BAD_REQUEST: ${serverMessage}`);
117
+ }
118
+ }
119
+ if (!response.ok) {
120
+ throw new Error(`HTTP_${response.status}`);
121
+ }
122
+ const data = await response.json();
123
+ return data.fields;
124
+ }
95
125
  async function getWorkItem(context, id, pat, extraFields) {
96
126
  const url = new URL(
97
127
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
@@ -307,15 +337,15 @@ async function resolvePat() {
307
337
  import { execSync } from "child_process";
308
338
  var patterns = [
309
339
  // HTTPS (current): https://dev.azure.com/{org}/{project}/_git/{repo}
310
- /^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/.+$/,
340
+ /^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+)$/,
311
341
  // HTTPS (legacy + DefaultCollection): https://{org}.visualstudio.com/DefaultCollection/{project}/_git/{repo}
312
- /^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/.+$/,
342
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/([^/]+)$/,
313
343
  // HTTPS (legacy): https://{org}.visualstudio.com/{project}/_git/{repo}
314
- /^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/.+$/,
344
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]+)$/,
315
345
  // SSH (current): git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
316
- /^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/.+$/,
346
+ /^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+)$/,
317
347
  // SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
318
- /^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/.+$/
348
+ /^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+)$/
319
349
  ];
320
350
  function parseAzdoRemote(url) {
321
351
  for (const pattern of patterns) {
@@ -343,6 +373,35 @@ function detectAzdoContext() {
343
373
  }
344
374
  return context;
345
375
  }
376
+ function parseRepoName(url) {
377
+ for (const pattern of patterns) {
378
+ const match = pattern.exec(url);
379
+ if (match) {
380
+ return match[3];
381
+ }
382
+ }
383
+ return null;
384
+ }
385
+ function detectRepoName() {
386
+ let remoteUrl;
387
+ try {
388
+ remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
389
+ } catch {
390
+ throw new Error('Not in a git repository. Check that git remote "origin" exists and try again.');
391
+ }
392
+ const repo = parseRepoName(remoteUrl);
393
+ if (!repo) {
394
+ throw new Error('Git remote "origin" is not an Azure DevOps URL. Check that origin points to Azure DevOps and try again.');
395
+ }
396
+ return repo;
397
+ }
398
+ function getCurrentBranch() {
399
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
400
+ if (branch === "HEAD") {
401
+ throw new Error("Not on a named branch. Check out a named branch and try again.");
402
+ }
403
+ return branch;
404
+ }
346
405
 
347
406
  // src/services/config-store.ts
348
407
  import fs from "fs";
@@ -1422,8 +1481,378 @@ function createUpsertCommand() {
1422
1481
  return command;
1423
1482
  }
1424
1483
 
1484
+ // src/commands/list-fields.ts
1485
+ import { Command as Command10 } from "commander";
1486
+ function stringifyValue(value) {
1487
+ if (value === null || value === void 0) return "";
1488
+ if (typeof value === "object") return JSON.stringify(value);
1489
+ return String(value);
1490
+ }
1491
+ function formatRichValue(raw) {
1492
+ const md = htmlToMarkdown(raw);
1493
+ const lines = md.split("\n").filter((l) => l.trim() !== "");
1494
+ const preview = lines.slice(0, 5);
1495
+ const suffix = lines.length > 5 ? `
1496
+ \u2026 (${lines.length - 5} more lines)` : "";
1497
+ return preview.join("\n ") + suffix;
1498
+ }
1499
+ function formatFieldList(fields) {
1500
+ const entries = Object.entries(fields).sort(([a], [b]) => a.localeCompare(b));
1501
+ const maxKeyLen = Math.min(
1502
+ Math.max(...entries.map(([k]) => k.length)),
1503
+ 50
1504
+ );
1505
+ return entries.map(([key, value]) => {
1506
+ const raw = stringifyValue(value);
1507
+ if (raw === "") return `${key.padEnd(maxKeyLen + 2)}(empty)`;
1508
+ if (typeof value === "string" && isHtml(value)) {
1509
+ const preview = formatRichValue(value);
1510
+ return `${key.padEnd(maxKeyLen + 2)}[rich text]
1511
+ ${preview}`;
1512
+ }
1513
+ return `${key.padEnd(maxKeyLen + 2)}${raw}`;
1514
+ }).join("\n");
1515
+ }
1516
+ function createListFieldsCommand() {
1517
+ const command = new Command10("list-fields");
1518
+ 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(
1519
+ async (idStr, options) => {
1520
+ const id = parseWorkItemId(idStr);
1521
+ validateOrgProjectPair(options);
1522
+ let context;
1523
+ try {
1524
+ context = resolveContext(options);
1525
+ const credential = await resolvePat();
1526
+ const fields = await getWorkItemFields(context, id, credential.pat);
1527
+ if (options.json) {
1528
+ process.stdout.write(JSON.stringify({ id, fields }, null, 2) + "\n");
1529
+ } else {
1530
+ process.stdout.write(`Work Item ${id} \u2014 ${Object.keys(fields).length} fields
1531
+
1532
+ `);
1533
+ process.stdout.write(formatFieldList(fields) + "\n");
1534
+ }
1535
+ } catch (err) {
1536
+ handleCommandError(err, id, context, "read");
1537
+ }
1538
+ }
1539
+ );
1540
+ return command;
1541
+ }
1542
+
1543
+ // src/commands/pr.ts
1544
+ import { Command as Command11 } from "commander";
1545
+
1546
+ // src/services/pr-client.ts
1547
+ function buildPullRequestsUrl(context, repo, sourceBranch, opts) {
1548
+ const url = new URL(
1549
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
1550
+ );
1551
+ url.searchParams.set("api-version", "7.1");
1552
+ url.searchParams.set("searchCriteria.sourceRefName", `refs/heads/${sourceBranch}`);
1553
+ if (opts?.status) {
1554
+ url.searchParams.set("searchCriteria.status", opts.status);
1555
+ }
1556
+ if (opts?.targetBranch) {
1557
+ url.searchParams.set("searchCriteria.targetRefName", `refs/heads/${opts.targetBranch}`);
1558
+ }
1559
+ return url;
1560
+ }
1561
+ function mapPullRequest(repo, pullRequest) {
1562
+ return {
1563
+ id: pullRequest.pullRequestId,
1564
+ title: pullRequest.title,
1565
+ repository: repo,
1566
+ sourceRefName: pullRequest.sourceRefName,
1567
+ targetRefName: pullRequest.targetRefName,
1568
+ status: pullRequest.status,
1569
+ createdBy: pullRequest.createdBy?.displayName ?? null,
1570
+ url: pullRequest._links.web.href
1571
+ };
1572
+ }
1573
+ function mapComment(comment) {
1574
+ const content = comment.content?.trim();
1575
+ if (comment.isDeleted || !content) {
1576
+ return null;
1577
+ }
1578
+ return {
1579
+ id: comment.id,
1580
+ author: comment.author?.displayName ?? null,
1581
+ content,
1582
+ publishedAt: comment.publishedDate ?? null
1583
+ };
1584
+ }
1585
+ function mapThread(thread) {
1586
+ if (thread.status !== "active" && thread.status !== "pending") {
1587
+ return null;
1588
+ }
1589
+ const comments = thread.comments.map(mapComment).filter((comment) => comment !== null);
1590
+ if (comments.length === 0) {
1591
+ return null;
1592
+ }
1593
+ return {
1594
+ id: thread.id,
1595
+ status: thread.status,
1596
+ threadContext: thread.threadContext?.filePath ?? null,
1597
+ comments
1598
+ };
1599
+ }
1600
+ async function readJsonResponse(response) {
1601
+ if (!response.ok) {
1602
+ throw new Error(`HTTP_${response.status}`);
1603
+ }
1604
+ return response.json();
1605
+ }
1606
+ async function listPullRequests(context, repo, pat, sourceBranch, opts) {
1607
+ const response = await fetchWithErrors(
1608
+ buildPullRequestsUrl(context, repo, sourceBranch, opts).toString(),
1609
+ { headers: authHeaders(pat) }
1610
+ );
1611
+ const data = await readJsonResponse(response);
1612
+ return data.value.map((pullRequest) => mapPullRequest(repo, pullRequest));
1613
+ }
1614
+ async function openPullRequest(context, repo, pat, sourceBranch, title, description) {
1615
+ const existing = await listPullRequests(context, repo, pat, sourceBranch, {
1616
+ status: "active",
1617
+ targetBranch: "develop"
1618
+ });
1619
+ if (existing.length === 1) {
1620
+ return {
1621
+ branch: sourceBranch,
1622
+ targetBranch: "develop",
1623
+ created: false,
1624
+ pullRequest: existing[0]
1625
+ };
1626
+ }
1627
+ if (existing.length > 1) {
1628
+ throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest) => pullRequest.id).join(",")}`);
1629
+ }
1630
+ const payload = {
1631
+ sourceRefName: `refs/heads/${sourceBranch}`,
1632
+ targetRefName: "refs/heads/develop",
1633
+ title,
1634
+ description
1635
+ };
1636
+ const url = new URL(
1637
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
1638
+ );
1639
+ url.searchParams.set("api-version", "7.1");
1640
+ const response = await fetchWithErrors(url.toString(), {
1641
+ method: "POST",
1642
+ headers: {
1643
+ ...authHeaders(pat),
1644
+ "Content-Type": "application/json"
1645
+ },
1646
+ body: JSON.stringify(payload)
1647
+ });
1648
+ const data = await readJsonResponse(response);
1649
+ return {
1650
+ branch: sourceBranch,
1651
+ targetBranch: "develop",
1652
+ created: true,
1653
+ pullRequest: mapPullRequest(repo, data)
1654
+ };
1655
+ }
1656
+ async function getPullRequestThreads(context, repo, pat, prId) {
1657
+ const url = new URL(
1658
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
1659
+ );
1660
+ url.searchParams.set("api-version", "7.1");
1661
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
1662
+ const data = await readJsonResponse(response);
1663
+ return data.value.map(mapThread).filter((thread) => thread !== null);
1664
+ }
1665
+
1666
+ // src/commands/pr.ts
1667
+ function formatBranchName(refName) {
1668
+ return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
1669
+ }
1670
+ function writeError(message) {
1671
+ process.stderr.write(`Error: ${message}
1672
+ `);
1673
+ process.exit(1);
1674
+ }
1675
+ function handlePrCommandError(err, context, mode = "read") {
1676
+ const error = err instanceof Error ? err : new Error(String(err));
1677
+ if (error.message === "AUTH_FAILED") {
1678
+ const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
1679
+ writeError(`Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.`);
1680
+ }
1681
+ if (error.message === "PERMISSION_DENIED") {
1682
+ writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`);
1683
+ }
1684
+ if (error.message === "NETWORK_ERROR") {
1685
+ writeError("Could not connect to Azure DevOps. Check your network connection.");
1686
+ }
1687
+ if (error.message === "NOT_FOUND") {
1688
+ writeError(`Azure DevOps repository not found in ${context?.org}/${context?.project}.`);
1689
+ }
1690
+ if (error.message.startsWith("HTTP_")) {
1691
+ writeError(`Azure DevOps request failed with ${error.message}.`);
1692
+ }
1693
+ writeError(error.message);
1694
+ }
1695
+ function formatPullRequestBlock(pullRequest) {
1696
+ return [
1697
+ `#${pullRequest.id} [${pullRequest.status}] ${pullRequest.title}`,
1698
+ `${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
1699
+ pullRequest.url
1700
+ ].join("\n");
1701
+ }
1702
+ function formatThreads(prId, title, threads) {
1703
+ const lines = [`Active comments for pull request #${prId}: ${title}`];
1704
+ for (const thread of threads) {
1705
+ lines.push("", `Thread #${thread.id} [${thread.status}] ${thread.threadContext ?? "(general)"}`);
1706
+ for (const comment of thread.comments) {
1707
+ lines.push(` ${comment.author ?? "Unknown"}: ${comment.content}`);
1708
+ }
1709
+ }
1710
+ return lines.join("\n");
1711
+ }
1712
+ async function resolvePrCommandContext(options) {
1713
+ const context = resolveContext(options);
1714
+ const repo = detectRepoName();
1715
+ const branch = getCurrentBranch();
1716
+ const credential = await resolvePat();
1717
+ return {
1718
+ context,
1719
+ repo,
1720
+ branch,
1721
+ pat: credential.pat
1722
+ };
1723
+ }
1724
+ function createPrStatusCommand() {
1725
+ const command = new Command11("status");
1726
+ 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) => {
1727
+ validateOrgProjectPair(options);
1728
+ let context;
1729
+ try {
1730
+ const resolved = await resolvePrCommandContext(options);
1731
+ context = resolved.context;
1732
+ const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch);
1733
+ const { branch, repo } = resolved;
1734
+ const result = { branch, repository: repo, pullRequests };
1735
+ if (options.json) {
1736
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1737
+ `);
1738
+ return;
1739
+ }
1740
+ if (pullRequests.length === 0) {
1741
+ process.stdout.write(`No pull requests found for branch ${branch}.
1742
+ `);
1743
+ return;
1744
+ }
1745
+ process.stdout.write(`${pullRequests.map(formatPullRequestBlock).join("\n\n")}
1746
+ `);
1747
+ } catch (err) {
1748
+ handlePrCommandError(err, context, "read");
1749
+ }
1750
+ });
1751
+ return command;
1752
+ }
1753
+ function createPrOpenCommand() {
1754
+ const command = new Command11("open");
1755
+ 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) => {
1756
+ validateOrgProjectPair(options);
1757
+ const title = options.title?.trim();
1758
+ if (!title) {
1759
+ writeError("--title is required for pull request creation.");
1760
+ }
1761
+ const description = options.description?.trim();
1762
+ if (!description) {
1763
+ writeError("--description is required for pull request creation.");
1764
+ }
1765
+ let context;
1766
+ try {
1767
+ const resolved = await resolvePrCommandContext(options);
1768
+ context = resolved.context;
1769
+ if (resolved.branch === "develop") {
1770
+ writeError("Pull request creation requires a source branch other than develop.");
1771
+ }
1772
+ const result = await openPullRequest(
1773
+ resolved.context,
1774
+ resolved.repo,
1775
+ resolved.pat,
1776
+ resolved.branch,
1777
+ title,
1778
+ description
1779
+ );
1780
+ if (options.json) {
1781
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1782
+ `);
1783
+ return;
1784
+ }
1785
+ if (result.created) {
1786
+ process.stdout.write(`Created pull request #${result.pullRequest.id}: ${result.pullRequest.title}
1787
+ ${result.pullRequest.url}
1788
+ `);
1789
+ return;
1790
+ }
1791
+ process.stdout.write(
1792
+ `Active pull request already exists for ${resolved.branch} -> develop: #${result.pullRequest.id}
1793
+ ${result.pullRequest.url}
1794
+ `
1795
+ );
1796
+ } catch (err) {
1797
+ if (err instanceof Error && err.message.startsWith("AMBIGUOUS_PRS:")) {
1798
+ const ids = err.message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
1799
+ writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
1800
+ }
1801
+ handlePrCommandError(err, context, "write");
1802
+ }
1803
+ });
1804
+ return command;
1805
+ }
1806
+ function createPrCommentsCommand() {
1807
+ const command = new Command11("comments");
1808
+ 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) => {
1809
+ validateOrgProjectPair(options);
1810
+ let context;
1811
+ try {
1812
+ const resolved = await resolvePrCommandContext(options);
1813
+ context = resolved.context;
1814
+ const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch, {
1815
+ status: "active"
1816
+ });
1817
+ if (pullRequests.length === 0) {
1818
+ writeError(`No active pull request found for branch ${resolved.branch}.`);
1819
+ }
1820
+ if (pullRequests.length > 1) {
1821
+ const ids = pullRequests.map((pullRequest2) => `#${pullRequest2.id}`).join(", ");
1822
+ writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
1823
+ }
1824
+ const pullRequest = pullRequests[0];
1825
+ const threads = await getPullRequestThreads(resolved.context, resolved.repo, resolved.pat, pullRequest.id);
1826
+ const result = { branch: resolved.branch, pullRequest, threads };
1827
+ if (options.json) {
1828
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1829
+ `);
1830
+ return;
1831
+ }
1832
+ if (threads.length === 0) {
1833
+ process.stdout.write(`Pull request #${pullRequest.id} has no active comments.
1834
+ `);
1835
+ return;
1836
+ }
1837
+ process.stdout.write(`${formatThreads(pullRequest.id, pullRequest.title, threads)}
1838
+ `);
1839
+ } catch (err) {
1840
+ handlePrCommandError(err, context, "read");
1841
+ }
1842
+ });
1843
+ return command;
1844
+ }
1845
+ function createPrCommand() {
1846
+ const command = new Command11("pr");
1847
+ command.description("Manage Azure DevOps pull requests");
1848
+ command.addCommand(createPrStatusCommand());
1849
+ command.addCommand(createPrOpenCommand());
1850
+ command.addCommand(createPrCommentsCommand());
1851
+ return command;
1852
+ }
1853
+
1425
1854
  // src/index.ts
1426
- var program = new Command10();
1855
+ var program = new Command12();
1427
1856
  program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
1428
1857
  program.addCommand(createGetItemCommand());
1429
1858
  program.addCommand(createClearPatCommand());
@@ -1434,6 +1863,8 @@ program.addCommand(createSetFieldCommand());
1434
1863
  program.addCommand(createGetMdFieldCommand());
1435
1864
  program.addCommand(createSetMdFieldCommand());
1436
1865
  program.addCommand(createUpsertCommand());
1866
+ program.addCommand(createListFieldsCommand());
1867
+ program.addCommand(createPrCommand());
1437
1868
  program.showHelpAfterError();
1438
1869
  program.parse();
1439
1870
  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-feature-macsecret.95",
3
+ "version": "0.2.0-feature-integrationtest.109",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {