@knowttl/ado-axi 0.0.0-stage → 0.4.8

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 (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -2
  3. package/SKILL.md +334 -0
  4. package/assets/ado-axi-header.png +0 -0
  5. package/dist/bin/ado-axi.d.ts +3 -0
  6. package/dist/bin/ado-axi.d.ts.map +1 -0
  7. package/dist/bin/ado-axi.js +192 -0
  8. package/dist/bin/ado-axi.js.map +1 -0
  9. package/dist/commands/agent.d.ts +3 -0
  10. package/dist/commands/agent.d.ts.map +1 -0
  11. package/dist/commands/agent.js +26 -0
  12. package/dist/commands/agent.js.map +1 -0
  13. package/dist/commands/api.d.ts +2 -0
  14. package/dist/commands/api.d.ts.map +1 -0
  15. package/dist/commands/api.js +161 -0
  16. package/dist/commands/api.js.map +1 -0
  17. package/dist/commands/classification.d.ts +2 -0
  18. package/dist/commands/classification.d.ts.map +1 -0
  19. package/dist/commands/classification.js +54 -0
  20. package/dist/commands/classification.js.map +1 -0
  21. package/dist/commands/config.d.ts +2 -0
  22. package/dist/commands/config.d.ts.map +1 -0
  23. package/dist/commands/config.js +109 -0
  24. package/dist/commands/config.js.map +1 -0
  25. package/dist/commands/doctor.d.ts +2 -0
  26. package/dist/commands/doctor.d.ts.map +1 -0
  27. package/dist/commands/doctor.js +67 -0
  28. package/dist/commands/doctor.js.map +1 -0
  29. package/dist/commands/home.d.ts +2 -0
  30. package/dist/commands/home.d.ts.map +1 -0
  31. package/dist/commands/home.js +132 -0
  32. package/dist/commands/home.js.map +1 -0
  33. package/dist/commands/pipeline.d.ts +2 -0
  34. package/dist/commands/pipeline.d.ts.map +1 -0
  35. package/dist/commands/pipeline.js +572 -0
  36. package/dist/commands/pipeline.js.map +1 -0
  37. package/dist/commands/pipelineAdmin.d.ts +3 -0
  38. package/dist/commands/pipelineAdmin.d.ts.map +1 -0
  39. package/dist/commands/pipelineAdmin.js +152 -0
  40. package/dist/commands/pipelineAdmin.js.map +1 -0
  41. package/dist/commands/pipelineArtifact.d.ts +3 -0
  42. package/dist/commands/pipelineArtifact.d.ts.map +1 -0
  43. package/dist/commands/pipelineArtifact.js +68 -0
  44. package/dist/commands/pipelineArtifact.js.map +1 -0
  45. package/dist/commands/policy.d.ts +3 -0
  46. package/dist/commands/policy.d.ts.map +1 -0
  47. package/dist/commands/policy.js +31 -0
  48. package/dist/commands/policy.js.map +1 -0
  49. package/dist/commands/pool.d.ts +3 -0
  50. package/dist/commands/pool.d.ts.map +1 -0
  51. package/dist/commands/pool.js +19 -0
  52. package/dist/commands/pool.js.map +1 -0
  53. package/dist/commands/pr.d.ts +9 -0
  54. package/dist/commands/pr.d.ts.map +1 -0
  55. package/dist/commands/pr.js +1028 -0
  56. package/dist/commands/pr.js.map +1 -0
  57. package/dist/commands/query.d.ts +2 -0
  58. package/dist/commands/query.d.ts.map +1 -0
  59. package/dist/commands/query.js +29 -0
  60. package/dist/commands/query.js.map +1 -0
  61. package/dist/commands/queue.d.ts +3 -0
  62. package/dist/commands/queue.d.ts.map +1 -0
  63. package/dist/commands/queue.js +21 -0
  64. package/dist/commands/queue.js.map +1 -0
  65. package/dist/commands/ref.d.ts +3 -0
  66. package/dist/commands/ref.d.ts.map +1 -0
  67. package/dist/commands/ref.js +204 -0
  68. package/dist/commands/ref.js.map +1 -0
  69. package/dist/commands/release.d.ts +3 -0
  70. package/dist/commands/release.d.ts.map +1 -0
  71. package/dist/commands/release.js +91 -0
  72. package/dist/commands/release.js.map +1 -0
  73. package/dist/commands/repo.d.ts +3 -0
  74. package/dist/commands/repo.d.ts.map +1 -0
  75. package/dist/commands/repo.js +261 -0
  76. package/dist/commands/repo.js.map +1 -0
  77. package/dist/commands/repoAdmin.d.ts +3 -0
  78. package/dist/commands/repoAdmin.d.ts.map +1 -0
  79. package/dist/commands/repoAdmin.js +195 -0
  80. package/dist/commands/repoAdmin.js.map +1 -0
  81. package/dist/commands/security.d.ts +3 -0
  82. package/dist/commands/security.d.ts.map +1 -0
  83. package/dist/commands/security.js +57 -0
  84. package/dist/commands/security.js.map +1 -0
  85. package/dist/commands/serviceEndpoint.d.ts +3 -0
  86. package/dist/commands/serviceEndpoint.d.ts.map +1 -0
  87. package/dist/commands/serviceEndpoint.js +29 -0
  88. package/dist/commands/serviceEndpoint.js.map +1 -0
  89. package/dist/commands/team.d.ts +3 -0
  90. package/dist/commands/team.d.ts.map +1 -0
  91. package/dist/commands/team.js +33 -0
  92. package/dist/commands/team.js.map +1 -0
  93. package/dist/commands/test.d.ts +2 -0
  94. package/dist/commands/test.d.ts.map +1 -0
  95. package/dist/commands/test.js +183 -0
  96. package/dist/commands/test.js.map +1 -0
  97. package/dist/commands/user.d.ts +3 -0
  98. package/dist/commands/user.d.ts.map +1 -0
  99. package/dist/commands/user.js +20 -0
  100. package/dist/commands/user.js.map +1 -0
  101. package/dist/commands/variableGroup.d.ts +3 -0
  102. package/dist/commands/variableGroup.d.ts.map +1 -0
  103. package/dist/commands/variableGroup.js +23 -0
  104. package/dist/commands/variableGroup.js.map +1 -0
  105. package/dist/commands/workItem.d.ts +12 -0
  106. package/dist/commands/workItem.d.ts.map +1 -0
  107. package/dist/commands/workItem.js +940 -0
  108. package/dist/commands/workItem.js.map +1 -0
  109. package/dist/commands/workItemAttachment.d.ts +3 -0
  110. package/dist/commands/workItemAttachment.d.ts.map +1 -0
  111. package/dist/commands/workItemAttachment.js +136 -0
  112. package/dist/commands/workItemAttachment.js.map +1 -0
  113. package/dist/help.d.ts +4 -0
  114. package/dist/help.d.ts.map +1 -0
  115. package/dist/help.js +383 -0
  116. package/dist/help.js.map +1 -0
  117. package/dist/lib/adoUrl.d.ts +19 -0
  118. package/dist/lib/adoUrl.d.ts.map +1 -0
  119. package/dist/lib/adoUrl.js +104 -0
  120. package/dist/lib/adoUrl.js.map +1 -0
  121. package/dist/lib/args.d.ts +14 -0
  122. package/dist/lib/args.d.ts.map +1 -0
  123. package/dist/lib/args.js +94 -0
  124. package/dist/lib/args.js.map +1 -0
  125. package/dist/lib/argv.d.ts +8 -0
  126. package/dist/lib/argv.d.ts.map +1 -0
  127. package/dist/lib/argv.js +35 -0
  128. package/dist/lib/argv.js.map +1 -0
  129. package/dist/lib/audit.d.ts +21 -0
  130. package/dist/lib/audit.d.ts.map +1 -0
  131. package/dist/lib/audit.js +128 -0
  132. package/dist/lib/audit.js.map +1 -0
  133. package/dist/lib/auth.d.ts +8 -0
  134. package/dist/lib/auth.d.ts.map +1 -0
  135. package/dist/lib/auth.js +127 -0
  136. package/dist/lib/auth.js.map +1 -0
  137. package/dist/lib/client.d.ts +32 -0
  138. package/dist/lib/client.d.ts.map +1 -0
  139. package/dist/lib/client.js +181 -0
  140. package/dist/lib/client.js.map +1 -0
  141. package/dist/lib/config.d.ts +39 -0
  142. package/dist/lib/config.d.ts.map +1 -0
  143. package/dist/lib/config.js +150 -0
  144. package/dist/lib/config.js.map +1 -0
  145. package/dist/lib/context.d.ts +5 -0
  146. package/dist/lib/context.d.ts.map +1 -0
  147. package/dist/lib/context.js +20 -0
  148. package/dist/lib/context.js.map +1 -0
  149. package/dist/lib/format.d.ts +17 -0
  150. package/dist/lib/format.d.ts.map +1 -0
  151. package/dist/lib/format.js +65 -0
  152. package/dist/lib/format.js.map +1 -0
  153. package/dist/lib/paths.d.ts +3 -0
  154. package/dist/lib/paths.d.ts.map +1 -0
  155. package/dist/lib/paths.js +13 -0
  156. package/dist/lib/paths.js.map +1 -0
  157. package/dist/lib/repositoryPath.d.ts +2 -0
  158. package/dist/lib/repositoryPath.d.ts.map +1 -0
  159. package/dist/lib/repositoryPath.js +26 -0
  160. package/dist/lib/repositoryPath.js.map +1 -0
  161. package/dist/lib/stdin.d.ts +2 -0
  162. package/dist/lib/stdin.d.ts.map +1 -0
  163. package/dist/lib/stdin.js +11 -0
  164. package/dist/lib/stdin.js.map +1 -0
  165. package/dist/lib/writes.d.ts +3 -0
  166. package/dist/lib/writes.d.ts.map +1 -0
  167. package/dist/lib/writes.js +37 -0
  168. package/dist/lib/writes.js.map +1 -0
  169. package/package.json +65 -4
@@ -0,0 +1,1028 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { assertKnownFlags, flagBool, flagList, flagNumber, flagString, parseArgs } from "../lib/args.js";
3
+ import { request } from "../lib/client.js";
4
+ import { requireProject } from "../lib/config.js";
5
+ import { profileFromArgs } from "../lib/context.js";
6
+ import { countLine, htmlToText, personName, pickFields, shortDate, truncate } from "../lib/format.js";
7
+ import { normalizeRepositoryPath } from "../lib/repositoryPath.js";
8
+ import { readStdinIfPiped } from "../lib/stdin.js";
9
+ const LIST_FLAGS = ["repo", "status", "creator", "reviewer", "target", "source", "limit"];
10
+ const GET_FLAGS = ["repo", "threads", "commits", "id"];
11
+ const CREATE_FLAGS = ["repo", "source", "target", "title", "description", "reviewers", "draft", "auto-complete", "work-items"];
12
+ const APPROVE_FLAGS = ["repo", "vote"];
13
+ const COMMENT_FLAGS = ["repo", "body", "thread", "file", "line"];
14
+ const UPDATE_FLAGS = ["repo", "title", "description", "draft", "auto-complete"];
15
+ const COMPLETE_FLAGS = ["repo", "squash", "delete-source-branch"];
16
+ const ABANDON_FLAGS = ["repo"];
17
+ const CHECK_FLAGS = ["repo", "limit"];
18
+ const DIFF_FLAGS = ["repo", "limit"];
19
+ const REVIEWER_FLAGS = ["repo", "reviewer", "required"];
20
+ const THREAD_LIST_FLAGS = ["repo", "limit", "id"];
21
+ const THREAD_STATUS_FLAGS = ["repo", "thread", "status", "id"];
22
+ const THREAD_REPLY_FLAGS = ["repo", "thread", "body", "resolve", "status", "id"];
23
+ const DEFAULT_SUMMARY_LIMIT = 20;
24
+ const VOTES = {
25
+ approve: 10,
26
+ "approve-with-suggestions": 5,
27
+ reset: 0,
28
+ "wait-for-author": -5,
29
+ reject: -10,
30
+ };
31
+ export async function prCommand(argv) {
32
+ const args = parseArgs(argv);
33
+ const sub = args.positionals[0] ?? "list";
34
+ const rest = { ...args, positionals: args.positionals.slice(1) };
35
+ switch (sub) {
36
+ case "list":
37
+ return listPrs(rest);
38
+ case "get":
39
+ case "view":
40
+ return getPr(rest);
41
+ case "comments":
42
+ case "threads":
43
+ return getPr({ ...rest, flags: { ...rest.flags, threads: true } });
44
+ case "thread":
45
+ return threadPr(rest);
46
+ case "create":
47
+ return createPr(rest);
48
+ case "update":
49
+ return updatePr(rest);
50
+ case "complete":
51
+ return completePr(rest);
52
+ case "abandon":
53
+ return abandonPr(rest);
54
+ case "checks":
55
+ return checksPr(rest);
56
+ case "diff":
57
+ case "changes":
58
+ return diffPr(rest);
59
+ case "reviewers":
60
+ return reviewerPr({ ...rest, positionals: ["list", ...rest.positionals] });
61
+ case "reviewer":
62
+ return reviewerPr(rest);
63
+ case "approve":
64
+ case "vote":
65
+ return votePr(rest);
66
+ case "comment":
67
+ return commentPr(rest);
68
+ default:
69
+ throw new AxiError(`unknown subcommand \`pr ${sub}\``, "VALIDATION_ERROR", [
70
+ "Subcommands: list | get | comments | create | update | complete | abandon | checks | diff | reviewer | approve | comment | thread",
71
+ "`pr comments <id>` is an alias for `pr get <id> --threads`",
72
+ "Run `ado-axi pr --help` for the full reference",
73
+ ]);
74
+ }
75
+ }
76
+ function refName(ref) {
77
+ return (ref ?? "").replace("refs/heads/", "");
78
+ }
79
+ function fullRef(value) {
80
+ return value.startsWith("refs/") ? value : `refs/heads/${value}`;
81
+ }
82
+ // Azure DevOps does not return a browsable link on pull-request responses:
83
+ // `_links` carries only `_apis` hrefs and `url` is the GUID REST endpoint.
84
+ // Derive the web URL from the repository instead, preferring `_links.web.href`
85
+ // in case a future API version starts supplying it.
86
+ function prWebUrl(pr, profile, project, repo) {
87
+ const direct = pr._links?.web?.href;
88
+ if (direct)
89
+ return direct;
90
+ const base = pr.repository?.webUrl;
91
+ if (base)
92
+ return `${base}/pullrequest/${pr.pullRequestId}`;
93
+ const repoName = pr.repository?.name ?? repo;
94
+ if (!repoName)
95
+ return "";
96
+ const org = encodeURIComponent(profile.org);
97
+ const projectName = pr.repository?.project?.name ?? project;
98
+ return `https://dev.azure.com/${org}/${encodeURIComponent(projectName)}/_git/${encodeURIComponent(repoName)}/pullrequest/${pr.pullRequestId}`;
99
+ }
100
+ function voteLabel(vote) {
101
+ switch (vote) {
102
+ case 10:
103
+ return "approved";
104
+ case 5:
105
+ return "approved-with-suggestions";
106
+ case -5:
107
+ return "waiting-for-author";
108
+ case -10:
109
+ return "rejected";
110
+ default:
111
+ return "no-vote";
112
+ }
113
+ }
114
+ function reviewSummary(reviewers) {
115
+ const list = reviewers ?? [];
116
+ if (list.length === 0)
117
+ return "no reviewers";
118
+ const approved = list.filter((r) => (r.vote ?? 0) >= 5).length;
119
+ const rejected = list.filter((r) => (r.vote ?? 0) < 0).length;
120
+ return rejected > 0
121
+ ? `${approved}/${list.length} approved, ${rejected} rejected`
122
+ : `${approved}/${list.length} approved`;
123
+ }
124
+ async function listPrs(args) {
125
+ assertKnownFlags(args, LIST_FLAGS, "pr list");
126
+ const profile = profileFromArgs(args);
127
+ const project = requireProject(profile, "pr list");
128
+ const repo = flagString(args, "repo");
129
+ const limit = flagNumber(args, "limit") ?? 30;
130
+ const requestedStatus = flagString(args, "status") ?? "active";
131
+ const draftOnly = requestedStatus === "draft";
132
+ const status = draftOnly ? "active" : requestedStatus;
133
+ const query = {
134
+ "searchCriteria.status": status,
135
+ $top: limit,
136
+ };
137
+ const creator = flagString(args, "creator");
138
+ if (creator)
139
+ query["searchCriteria.creatorId"] = await resolveIdentityId(profile, creator);
140
+ const reviewer = flagString(args, "reviewer");
141
+ if (reviewer)
142
+ query["searchCriteria.reviewerId"] = await resolveIdentityId(profile, reviewer);
143
+ const target = flagString(args, "target");
144
+ if (target)
145
+ query["searchCriteria.targetRefName"] = fullRef(target);
146
+ const source = flagString(args, "source");
147
+ if (source)
148
+ query["searchCriteria.sourceRefName"] = fullRef(source);
149
+ const path = repo
150
+ ? `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
151
+ : "_apis/git/pullrequests";
152
+ let result = await request(profile, {
153
+ path,
154
+ project,
155
+ query: draftOnly ? { ...query, $skip: 0 } : query,
156
+ });
157
+ const prs = (result.value ?? []).filter((pr) => !draftOnly || pr.isDraft);
158
+ let skip = (result.value ?? []).length;
159
+ while (draftOnly && prs.length < limit && (result.value ?? []).length === limit) {
160
+ result = await request(profile, {
161
+ path,
162
+ project,
163
+ query: { ...query, $skip: skip },
164
+ });
165
+ const page = result.value ?? [];
166
+ prs.push(...page.filter((pr) => pr.isDraft));
167
+ skip += page.length;
168
+ }
169
+ prs.splice(limit);
170
+ if (prs.length === 0) {
171
+ return {
172
+ "pull-requests": `0 ${requestedStatus} pull requests found in ${project}${repo ? `/${repo}` : ""}`,
173
+ help: [
174
+ "Run `ado-axi pr list --status completed` to see merged pull requests",
175
+ "Run `ado-axi pr list --status all` to include abandoned ones",
176
+ 'Run `ado-axi pr create --repo <repo> --source <branch> --title "<title>"` to open one',
177
+ ],
178
+ };
179
+ }
180
+ const rows = prs.map((pr) => ({
181
+ id: pr.pullRequestId,
182
+ title: pr.title ?? "",
183
+ repo: pr.repository?.name ?? repo ?? "",
184
+ author: personName(pr.createdBy),
185
+ status: pr.isDraft ? "draft" : (pr.status ?? ""),
186
+ reviews: reviewSummary(pr.reviewers),
187
+ target: refName(pr.targetRefName),
188
+ created: shortDate(pr.creationDate),
189
+ }));
190
+ return {
191
+ org: profile.org,
192
+ project,
193
+ count: countLine(rows.length, draftOnly ? undefined : result.count, `${requestedStatus} pull requests`),
194
+ "pull-requests": pickFields(rows, flagList(args, "fields")),
195
+ help: [
196
+ "Run `ado-axi pr get <id>` for description, reviewers, and merge status",
197
+ "Run `ado-axi pr approve <id>` to vote approve",
198
+ 'Run `ado-axi pr comment <id> --body "..."` to start a thread',
199
+ ],
200
+ };
201
+ }
202
+ async function fetchPr(profile, id, repo, project) {
203
+ if (repo) {
204
+ return request(profile, {
205
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}`,
206
+ project,
207
+ });
208
+ }
209
+ return request(profile, { path: `_apis/git/pullrequests/${id}`, project });
210
+ }
211
+ async function getPr(args) {
212
+ assertKnownFlags(args, GET_FLAGS, "pr get");
213
+ const profile = profileFromArgs(args);
214
+ const project = requireProject(profile, "pr get");
215
+ const id = requirePrId(args);
216
+ const pr = await fetchPr(profile, id, flagString(args, "repo"), project);
217
+ const repoName = pr.repository?.name ?? "";
218
+ const description = htmlToText(pr.description ?? "");
219
+ const body = truncate(description, flagBool(args, "full") ? Number.MAX_SAFE_INTEGER : 1200);
220
+ const out = {
221
+ "pull-request": {
222
+ id: pr.pullRequestId,
223
+ title: pr.title ?? "",
224
+ repo: repoName,
225
+ status: pr.isDraft ? "draft" : (pr.status ?? ""),
226
+ author: personName(pr.createdBy),
227
+ source: refName(pr.sourceRefName),
228
+ target: refName(pr.targetRefName),
229
+ merge: pr.mergeStatus ?? "",
230
+ reviews: reviewSummary(pr.reviewers),
231
+ created: shortDate(pr.creationDate),
232
+ url: prWebUrl(pr, profile, project, flagString(args, "repo")),
233
+ description: body.text,
234
+ },
235
+ reviewers: (pr.reviewers ?? []).map((r) => ({
236
+ name: r.displayName ?? "",
237
+ vote: voteLabel(r.vote),
238
+ required: Boolean(r.isRequired),
239
+ })),
240
+ };
241
+ const help = [];
242
+ if (flagBool(args, "threads") && repoName) {
243
+ const commentLimit = flagBool(args, "full") ? Number.MAX_SAFE_INTEGER : 300;
244
+ const threads = await request(profile, {
245
+ path: `_apis/git/repositories/${encodeURIComponent(repoName)}/pullrequests/${id}/threads`,
246
+ project,
247
+ });
248
+ const visible = (threads.value ?? []).filter((t) => (t.comments ?? []).some((c) => c.commentType !== "system"));
249
+ out.threads = visible.map((t) => ({
250
+ id: t.id,
251
+ status: t.status ?? "",
252
+ file: t.threadContext?.filePath ?? "",
253
+ comments: (t.comments ?? [])
254
+ .filter((c) => c.commentType !== "system")
255
+ .map((c) => `${personName(c.author)}: ${truncate(htmlToText(c.content ?? ""), commentLimit).text}`)
256
+ .join(" | "),
257
+ }));
258
+ if (visible.length === 0)
259
+ out.threads = "0 non-system comment threads on this pull request";
260
+ else if (commentLimit !== Number.MAX_SAFE_INTEGER)
261
+ help.push(`Run \`ado-axi pr get ${id} --threads --full\` for complete comment text`);
262
+ }
263
+ if (body.truncated)
264
+ help.push(`Run \`ado-axi pr get ${id} --full\` for the complete description`);
265
+ if (!flagBool(args, "threads"))
266
+ help.push(`Run \`ado-axi pr get ${id} --threads\` to read review comments`);
267
+ if (help.length > 0)
268
+ out.help = help;
269
+ return out;
270
+ }
271
+ function requirePrId(args) {
272
+ const raw = args.positionals[0] ?? flagString(args, "id");
273
+ const id = Number(raw);
274
+ if (!raw || !Number.isFinite(id)) {
275
+ throw new AxiError("a numeric pull request id is required", "VALIDATION_ERROR", [
276
+ "Usage: ado-axi pr get <id> (or --id <id>)",
277
+ "Run `ado-axi pr list` to find ids",
278
+ ]);
279
+ }
280
+ return id;
281
+ }
282
+ async function resolveIdentityId(profile, value) {
283
+ if (value === "@me") {
284
+ const me = await currentUser(profile);
285
+ return me.id;
286
+ }
287
+ if (/^[0-9a-f-]{36}$/i.test(value))
288
+ return value;
289
+ const result = await request(profile, {
290
+ host: "vssps",
291
+ path: "_apis/identities",
292
+ query: { searchFilter: "General", filterValue: value },
293
+ apiVersion: "7.1",
294
+ });
295
+ const match = result.value?.[0];
296
+ if (!match?.id) {
297
+ throw new AxiError(`no Azure DevOps identity matched '${value}'`, "NOT_FOUND", [
298
+ "Pass the full display name, the sign-in address, or the identity GUID",
299
+ "Use @me for the authenticated identity",
300
+ ]);
301
+ }
302
+ return match.id;
303
+ }
304
+ export async function currentUser(profile) {
305
+ const data = await request(profile, { path: "_apis/connectionData", apiVersion: "7.1-preview" });
306
+ const user = data.authenticatedUser;
307
+ if (!user?.id) {
308
+ throw new AxiError("could not determine the authenticated identity", "AUTH_REQUIRED", [
309
+ "Run `ado-axi doctor` to verify authentication",
310
+ ]);
311
+ }
312
+ return { id: user.id, name: user.providerDisplayName ?? "" };
313
+ }
314
+ async function createPr(args) {
315
+ assertKnownFlags(args, CREATE_FLAGS, "pr create");
316
+ const profile = profileFromArgs(args);
317
+ const project = requireProject(profile, "pr create");
318
+ const repo = flagString(args, "repo");
319
+ const source = flagString(args, "source");
320
+ const title = flagString(args, "title");
321
+ if (!repo || !source || !title) {
322
+ throw new AxiError("--repo, --source and --title are required", "VALIDATION_ERROR", [
323
+ 'Usage: ado-axi pr create --repo <repo> --source <branch> [--target <branch>] --title "..." [--description "..."]',
324
+ "Run `ado-axi repo list` to see repository names",
325
+ ]);
326
+ }
327
+ const target = flagString(args, "target") ?? "main";
328
+ const reviewers = flagList(args, "reviewers");
329
+ const resolvedReviewers = reviewers
330
+ ? await Promise.all(reviewers.map(async (r) => ({ id: await resolveIdentityId(profile, r) })))
331
+ : undefined;
332
+ const workItems = flagList(args, "work-items");
333
+ const description = flagString(args, "description") ?? (await readStdinIfPiped())?.toString("utf8");
334
+ const created = await request(profile, {
335
+ method: "POST",
336
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`,
337
+ project,
338
+ body: {
339
+ sourceRefName: fullRef(source),
340
+ targetRefName: fullRef(target),
341
+ title,
342
+ description: description ?? "",
343
+ isDraft: flagBool(args, "draft"),
344
+ reviewers: resolvedReviewers,
345
+ workItemRefs: workItems?.map((id) => ({ id })),
346
+ },
347
+ });
348
+ return {
349
+ created: {
350
+ id: created.pullRequestId,
351
+ title: created.title ?? title,
352
+ repo,
353
+ source: refName(created.sourceRefName),
354
+ target: refName(created.targetRefName),
355
+ status: created.isDraft ? "draft" : (created.status ?? ""),
356
+ url: prWebUrl(created, profile, project, repo),
357
+ },
358
+ help: [
359
+ `Run \`ado-axi pr get ${created.pullRequestId}\` to view it`,
360
+ `Run \`ado-axi pr approve ${created.pullRequestId}\` to vote approve`,
361
+ ],
362
+ };
363
+ }
364
+ function booleanFlag(args, name) {
365
+ const value = args.flags[name];
366
+ if (value === undefined)
367
+ return undefined;
368
+ if (value === true || value === "true")
369
+ return true;
370
+ if (value === "false")
371
+ return false;
372
+ throw new AxiError(`--${name} expects true or false`, "VALIDATION_ERROR", [
373
+ `Use --${name} or --${name} true to enable it; use --${name} false to disable it`,
374
+ ]);
375
+ }
376
+ function prSummary(pr) {
377
+ return {
378
+ id: pr.pullRequestId,
379
+ title: pr.title ?? "",
380
+ status: pr.isDraft ? "draft" : (pr.status ?? ""),
381
+ merge: pr.mergeStatus ?? "",
382
+ "auto-complete": Boolean(pr.autoCompleteSetBy),
383
+ };
384
+ }
385
+ async function updatePr(args) {
386
+ assertKnownFlags(args, UPDATE_FLAGS, "pr update");
387
+ const profile = profileFromArgs(args);
388
+ const project = requireProject(profile, "pr update");
389
+ const id = requirePrId(args);
390
+ const pr = await fetchPr(profile, id, flagString(args, "repo"), project);
391
+ const requested = {};
392
+ const changed = [];
393
+ const title = flagString(args, "title");
394
+ if (title !== undefined && title !== pr.title) {
395
+ requested.title = title;
396
+ changed.push("title");
397
+ }
398
+ let description = flagString(args, "description");
399
+ if (description === undefined)
400
+ description = (await readStdinIfPiped())?.toString("utf8");
401
+ if (description !== undefined && description !== (pr.description ?? "")) {
402
+ requested.description = description;
403
+ changed.push("description");
404
+ }
405
+ const draft = booleanFlag(args, "draft");
406
+ if (draft !== undefined && draft !== Boolean(pr.isDraft)) {
407
+ requested.isDraft = draft;
408
+ changed.push("draft");
409
+ }
410
+ const autoComplete = booleanFlag(args, "auto-complete");
411
+ if (autoComplete !== undefined && autoComplete !== Boolean(pr.autoCompleteSetBy)) {
412
+ requested.autoCompleteSetBy = autoComplete ? { id: (await currentUser(profile)).id } : null;
413
+ changed.push("auto-complete");
414
+ }
415
+ if (changed.length === 0) {
416
+ const supplied = title !== undefined || description !== undefined || draft !== undefined || autoComplete !== undefined;
417
+ if (supplied)
418
+ return { "pull-request": `#${id} already has the requested values (no-op)`, state: prSummary(pr) };
419
+ throw new AxiError("no changes requested", "VALIDATION_ERROR", [
420
+ `Usage: ado-axi pr update ${id} [--title "..."] [--description "..."] [--draft true|false] [--auto-complete true|false]`,
421
+ "Pipe a multiline description to stdin when --description is omitted",
422
+ ]);
423
+ }
424
+ const repo = pr.repository?.name ?? flagString(args, "repo");
425
+ if (!repo) {
426
+ throw new AxiError(`could not resolve the repository for pull request ${id}`, "NOT_FOUND", [
427
+ "Pass --repo <repo> explicitly",
428
+ ]);
429
+ }
430
+ const updated = await request(profile, {
431
+ method: "PATCH",
432
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}`,
433
+ project,
434
+ body: requested,
435
+ });
436
+ return { updated: { fields: changed.join(", "), ...prSummary(updated) } };
437
+ }
438
+ async function abandonPr(args) {
439
+ assertKnownFlags(args, ABANDON_FLAGS, "pr abandon");
440
+ const profile = profileFromArgs(args);
441
+ const project = requireProject(profile, "pr abandon");
442
+ const id = requirePrId(args);
443
+ const pr = await fetchPr(profile, id, flagString(args, "repo"), project);
444
+ if (pr.status === "abandoned") {
445
+ return { "pull-request": `#${id} is already abandoned (no-op)`, outcome: "abandoned" };
446
+ }
447
+ if (pr.status === "completed") {
448
+ throw new AxiError(`pull request #${id} is completed and cannot be abandoned`, "VALIDATION_ERROR", [
449
+ "Only active pull requests can be abandoned",
450
+ ]);
451
+ }
452
+ const repo = pr.repository?.name ?? flagString(args, "repo");
453
+ if (!repo) {
454
+ throw new AxiError(`could not resolve the repository for pull request ${id}`, "NOT_FOUND", [
455
+ "Pass --repo <repo> explicitly",
456
+ ]);
457
+ }
458
+ const abandoned = await request(profile, {
459
+ method: "PATCH",
460
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}`,
461
+ project,
462
+ body: { status: "abandoned" },
463
+ });
464
+ return { abandoned: { id, status: abandoned.status ?? "" } };
465
+ }
466
+ function completionError(error, id) {
467
+ if (error instanceof AxiError && /conflict|merge conflict|rebase/i.test(error.message)) {
468
+ throw new AxiError(`pull request #${id} has merge conflicts`, "PR_CONFLICT", [
469
+ "Resolve the source/target branch conflicts and retry; policies were not bypassed",
470
+ ]);
471
+ }
472
+ if (error instanceof AxiError && /policy|policies|required reviewer|minimum number|permission.*complete/i.test(error.message)) {
473
+ throw new AxiError(`pull request #${id} is blocked by policy`, "POLICY_BLOCKED", [
474
+ `Run \`ado-axi pr checks ${id}\` for failed or pending requirements`,
475
+ "No policy bypass or force completion was attempted",
476
+ ]);
477
+ }
478
+ if (error instanceof AxiError && ["API_ERROR", "VALIDATION_ERROR", "CONFLICT", "PRECONDITION_FAILED"].includes(error.code)) {
479
+ throw new AxiError(`pull request #${id} completion failed: ${error.message}`, "PR_COMPLETION_FAILED", [
480
+ `Run \`ado-axi pr get ${id}\` and \`ado-axi pr checks ${id}\` before retrying`,
481
+ "No policy bypass or force completion was attempted",
482
+ ]);
483
+ }
484
+ throw error;
485
+ }
486
+ async function completePr(args) {
487
+ assertKnownFlags(args, COMPLETE_FLAGS, "pr complete");
488
+ const profile = profileFromArgs(args);
489
+ const project = requireProject(profile, "pr complete");
490
+ const id = requirePrId(args);
491
+ const pr = await fetchPr(profile, id, flagString(args, "repo"), project);
492
+ if (pr.status === "completed") {
493
+ return { "pull-request": `#${id} is already completed (no-op)`, outcome: "completed" };
494
+ }
495
+ if (pr.status === "abandoned") {
496
+ throw new AxiError(`pull request #${id} is abandoned and cannot be completed`, "VALIDATION_ERROR", [
497
+ "Reactivate the pull request before attempting completion",
498
+ ]);
499
+ }
500
+ if (/conflict/i.test(pr.mergeStatus ?? ""))
501
+ completionError(new AxiError("merge conflict", "API_ERROR"), id);
502
+ const repo = pr.repository?.name ?? flagString(args, "repo");
503
+ const commitId = pr.lastMergeSourceCommit?.commitId;
504
+ if (!repo || !commitId) {
505
+ throw new AxiError(`pull request #${id} lacks repository or source commit data`, "PRECONDITION_FAILED", [
506
+ "Re-read the pull request and retry; completion requires the current source version",
507
+ ]);
508
+ }
509
+ const squash = booleanFlag(args, "squash") ?? false;
510
+ const deleteSourceBranch = booleanFlag(args, "delete-source-branch") ?? false;
511
+ let completed;
512
+ try {
513
+ completed = await request(profile, {
514
+ method: "PATCH",
515
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}`,
516
+ project,
517
+ body: {
518
+ status: "completed",
519
+ lastMergeSourceCommit: { commitId },
520
+ completionOptions: {
521
+ mergeStrategy: squash ? "squash" : "noFastForward",
522
+ deleteSourceBranch,
523
+ bypassPolicy: false,
524
+ },
525
+ },
526
+ });
527
+ }
528
+ catch (error) {
529
+ completionError(error, id);
530
+ }
531
+ const outcome = completed.status === "completed"
532
+ ? "completed"
533
+ : completed.autoCompleteSetBy || completed.mergeStatus === "queued"
534
+ ? "queued"
535
+ : /conflict/i.test(completed.mergeStatus ?? "")
536
+ ? "conflict"
537
+ : "failed";
538
+ if (outcome === "conflict")
539
+ completionError(new AxiError("merge conflict", "API_ERROR"), id);
540
+ if (outcome === "failed")
541
+ process.exitCode = 1;
542
+ const out = {
543
+ completion: {
544
+ id,
545
+ outcome,
546
+ status: completed.status ?? "",
547
+ merge: completed.mergeStatus ?? "",
548
+ strategy: squash ? "squash" : "no-fast-forward",
549
+ "source-branch": deleteSourceBranch ? "delete requested" : "preserved",
550
+ },
551
+ };
552
+ if (outcome === "queued")
553
+ out.help = [`Run \`ado-axi pr checks ${id}\` to see what completion is waiting for`];
554
+ return out;
555
+ }
556
+ function checkResult(value) {
557
+ const state = (value ?? "").toLowerCase();
558
+ if (["approved", "succeeded", "success", "notapplicable"].includes(state))
559
+ return "passed";
560
+ if (["rejected", "broken", "failed", "failure", "error"].includes(state))
561
+ return "failed";
562
+ return "pending";
563
+ }
564
+ async function checksPr(args) {
565
+ assertKnownFlags(args, CHECK_FLAGS, "pr checks");
566
+ const profile = profileFromArgs(args);
567
+ const project = requireProject(profile, "pr checks");
568
+ const id = requirePrId(args);
569
+ const pr = await fetchPr(profile, id, flagString(args, "repo"), project);
570
+ const repo = pr.repository?.name ?? flagString(args, "repo");
571
+ if (!repo)
572
+ throw new AxiError(`could not resolve the repository for pull request ${id}`, "NOT_FOUND");
573
+ let projectId = pr.repository?.project?.id;
574
+ if (!projectId) {
575
+ const projectInfo = await request(profile, { path: `_apis/projects/${encodeURIComponent(project)}` });
576
+ projectId = projectInfo.id;
577
+ }
578
+ const policies = projectId
579
+ ? await request(profile, {
580
+ path: "_apis/policy/evaluations",
581
+ project,
582
+ apiVersion: "7.1-preview.1",
583
+ query: { artifactId: `vstfs:///CodeReview/CodeReviewId/${projectId}/${id}` },
584
+ })
585
+ : { value: [] };
586
+ const statuses = await request(profile, {
587
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}/statuses`,
588
+ project,
589
+ apiVersion: "7.1-preview.1",
590
+ });
591
+ const checks = [
592
+ ...(policies.value ?? []).map((item) => ({
593
+ type: "policy",
594
+ name: item.configuration?.settings?.displayName ?? item.configuration?.type?.displayName ?? "policy",
595
+ result: checkResult(item.status),
596
+ status: item.status ?? "",
597
+ url: "",
598
+ })),
599
+ ...(statuses.value ?? []).map((item) => ({
600
+ type: item.context?.genre ?? "status",
601
+ name: item.context?.name ?? item.description ?? "status",
602
+ result: checkResult(item.state),
603
+ status: item.state ?? "",
604
+ url: item.targetUrl ?? "",
605
+ })),
606
+ ];
607
+ if (checks.length === 0) {
608
+ return { checks: `0 checks registered for pull request #${id}`, passed: 0, failed: 0, pending: 0 };
609
+ }
610
+ const passed = checks.filter((item) => item.result === "passed").length;
611
+ const failed = checks.filter((item) => item.result === "failed").length;
612
+ const pending = checks.filter((item) => item.result === "pending").length;
613
+ const actionable = checks.filter((item) => item.result !== "passed");
614
+ const requestedLimit = flagNumber(args, "limit");
615
+ if (requestedLimit !== undefined && requestedLimit < 1) {
616
+ throw new AxiError("--limit must be at least 1", "VALIDATION_ERROR");
617
+ }
618
+ const limit = flagBool(args, "full") ? actionable.length : (requestedLimit ?? 10);
619
+ const out = {
620
+ "pull-request": id,
621
+ total: checks.length,
622
+ passed,
623
+ failed,
624
+ pending,
625
+ outcome: failed > 0 ? "blocked" : pending > 0 ? "pending" : "ready",
626
+ actionable: actionable.length === 0 ? "0 failed or pending checks" : actionable.slice(0, limit),
627
+ };
628
+ if (actionable.length > limit) {
629
+ out.help = [`Showing ${limit} of ${actionable.length} actionable checks; run \`ado-axi pr checks ${id} --full\` for all`];
630
+ }
631
+ return out;
632
+ }
633
+ async function diffPr(args) {
634
+ assertKnownFlags(args, DIFF_FLAGS, "pr diff");
635
+ const profile = profileFromArgs(args);
636
+ const project = requireProject(profile, "pr diff");
637
+ const id = requirePrId(args);
638
+ const pr = await fetchPr(profile, id, flagString(args, "repo"), project);
639
+ const repo = pr.repository?.name ?? flagString(args, "repo");
640
+ if (!repo)
641
+ throw new AxiError(`could not resolve the repository for pull request ${id}`, "NOT_FOUND");
642
+ const iterations = await request(profile, {
643
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}/iterations`,
644
+ project,
645
+ });
646
+ const iteration = Math.max(...(iterations.value ?? []).map((item) => item.id ?? 0));
647
+ if (!Number.isFinite(iteration) || iteration < 1)
648
+ return { changes: `0 changed paths found for pull request #${id}` };
649
+ const full = flagBool(args, "full");
650
+ const limit = full ? Number.MAX_SAFE_INTEGER : (flagNumber(args, "limit") ?? DEFAULT_SUMMARY_LIMIT);
651
+ if (limit < 1)
652
+ throw new AxiError("--limit must be at least 1", "VALIDATION_ERROR");
653
+ const entries = [];
654
+ let skip = 0;
655
+ let nextSkip;
656
+ let total;
657
+ do {
658
+ const top = full ? 1000 : limit + 1;
659
+ const page = await request(profile, {
660
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}/iterations/${iteration}/changes`,
661
+ project,
662
+ query: { $top: top, $skip: skip },
663
+ });
664
+ entries.push(...(page.changeEntries ?? []));
665
+ total = page.count ?? total;
666
+ nextSkip = page.nextSkip;
667
+ if (!full || nextSkip === undefined || (page.changeEntries ?? []).length === 0)
668
+ break;
669
+ skip = nextSkip;
670
+ } while (true);
671
+ const shown = full ? entries : entries.slice(0, limit);
672
+ if (shown.length === 0)
673
+ return { changes: `0 changed paths found for pull request #${id}` };
674
+ const truncated = !full && (entries.length > limit || nextSkip !== undefined || (total ?? 0) > limit);
675
+ const out = {
676
+ "pull-request": id,
677
+ iteration,
678
+ count: total !== undefined ? countLine(shown.length, total, "changed paths") : `${shown.length}${truncated ? "+" : ""} changed paths`,
679
+ changes: shown.map((entry) => ({
680
+ path: entry.item?.path ?? "",
681
+ change: entry.changeType ?? "",
682
+ from: entry.item?.originalPath ?? "",
683
+ object: (entry.item?.objectId ?? "").slice(0, 12),
684
+ })),
685
+ };
686
+ if (truncated)
687
+ out.help = [`Result truncated; run \`ado-axi pr diff ${id} --full\` or increase --limit`];
688
+ return out;
689
+ }
690
+ async function reviewerPr(args) {
691
+ const operation = args.positionals[0] ?? "list";
692
+ const nested = { ...args, positionals: args.positionals.slice(1) };
693
+ assertKnownFlags(nested, REVIEWER_FLAGS, `pr reviewer ${operation}`);
694
+ if (!["list", "add", "remove"].includes(operation)) {
695
+ throw new AxiError(`unknown reviewer operation '${operation}'`, "VALIDATION_ERROR", [
696
+ "Usage: ado-axi pr reviewer list|add|remove <id> [--reviewer <identity>]",
697
+ ]);
698
+ }
699
+ const profile = profileFromArgs(nested);
700
+ const project = requireProject(profile, `pr reviewer ${operation}`);
701
+ const id = requirePrId(nested);
702
+ const pr = await fetchPr(profile, id, flagString(nested, "repo"), project);
703
+ const repo = pr.repository?.name ?? flagString(nested, "repo");
704
+ if (!repo)
705
+ throw new AxiError(`could not resolve the repository for pull request ${id}`, "NOT_FOUND");
706
+ if (operation === "list") {
707
+ const response = await request(profile, {
708
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}/reviewers`,
709
+ project,
710
+ });
711
+ const reviewers = response.value ?? [];
712
+ return reviewers.length === 0
713
+ ? { reviewers: `0 reviewers on pull request #${id}` }
714
+ : {
715
+ "pull-request": id,
716
+ count: reviewers.length,
717
+ reviewers: reviewers.map((reviewer) => ({
718
+ id: reviewer.id ?? "",
719
+ name: reviewer.displayName ?? "",
720
+ vote: voteLabel(reviewer.vote),
721
+ required: Boolean(reviewer.isRequired),
722
+ })),
723
+ };
724
+ }
725
+ const identity = flagString(nested, "reviewer");
726
+ if (!identity)
727
+ throw new AxiError("--reviewer <identity> is required", "VALIDATION_ERROR");
728
+ const reviewerId = await resolveIdentityId(profile, identity);
729
+ const existing = (pr.reviewers ?? []).find((reviewer) => reviewer.id?.toLowerCase() === reviewerId.toLowerCase());
730
+ if (operation === "add") {
731
+ if (existing)
732
+ return { reviewer: `${identity} is already a reviewer on #${id} (no-op)` };
733
+ await request(profile, {
734
+ method: "PUT",
735
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}/reviewers/${reviewerId}`,
736
+ project,
737
+ body: { vote: 0, isRequired: booleanFlag(nested, "required") ?? false },
738
+ });
739
+ return { reviewer: { "pull-request": id, id: reviewerId, added: true } };
740
+ }
741
+ if (!existing)
742
+ return { reviewer: `${identity} is not a reviewer on #${id} (no-op)` };
743
+ await request(profile, {
744
+ method: "DELETE",
745
+ path: `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${id}/reviewers/${reviewerId}`,
746
+ project,
747
+ });
748
+ return { reviewer: { "pull-request": id, id: reviewerId, removed: true } };
749
+ }
750
+ async function votePr(args) {
751
+ assertKnownFlags(args, APPROVE_FLAGS, "pr approve");
752
+ const profile = profileFromArgs(args);
753
+ const project = requireProject(profile, "pr approve");
754
+ const id = requirePrId(args);
755
+ const voteName = flagString(args, "vote") ?? "approve";
756
+ const vote = VOTES[voteName];
757
+ if (vote === undefined) {
758
+ throw new AxiError(`unknown vote '${voteName}'`, "VALIDATION_ERROR", [
759
+ `Valid votes: ${Object.keys(VOTES).join(", ")}`,
760
+ ]);
761
+ }
762
+ const pr = await fetchPr(profile, id, flagString(args, "repo"), project);
763
+ const repoName = pr.repository?.name;
764
+ if (!repoName) {
765
+ throw new AxiError(`could not resolve the repository for pull request ${id}`, "NOT_FOUND", [
766
+ "Pass --repo <repo> explicitly",
767
+ ]);
768
+ }
769
+ const me = await currentUser(profile);
770
+ const existing = (pr.reviewers ?? []).find((r) => r.id === me.id);
771
+ if (existing && existing.vote === vote) {
772
+ return {
773
+ "pull-request": `#${id} already ${voteLabel(vote)} by ${me.name || "you"} (no-op)`,
774
+ };
775
+ }
776
+ await request(profile, {
777
+ method: "PUT",
778
+ path: `_apis/git/repositories/${encodeURIComponent(repoName)}/pullrequests/${id}/reviewers/${me.id}`,
779
+ project,
780
+ body: { vote },
781
+ });
782
+ return {
783
+ vote: { "pull-request": id, repo: repoName, reviewer: me.name || me.id, result: voteLabel(vote) },
784
+ };
785
+ }
786
+ async function commentPr(args) {
787
+ assertKnownFlags(args, COMMENT_FLAGS, "pr comment");
788
+ const profile = profileFromArgs(args);
789
+ const project = requireProject(profile, "pr comment");
790
+ const id = requirePrId(args);
791
+ const body = flagString(args, "body") ?? (await readStdinIfPiped())?.toString("utf8");
792
+ if (!body) {
793
+ throw new AxiError("--body is required", "VALIDATION_ERROR", [
794
+ `Usage: ado-axi pr comment ${id} --body "..." [--file <path> --line <n>] [--thread <id>]`,
795
+ "Pipe the comment to stdin when omitting --body",
796
+ ]);
797
+ }
798
+ const rawFile = flagString(args, "file");
799
+ const file = rawFile ? normalizeRepositoryPath(rawFile) : undefined;
800
+ const pr = await fetchPr(profile, id, flagString(args, "repo"), project);
801
+ const repoName = pr.repository?.name;
802
+ if (!repoName) {
803
+ throw new AxiError(`could not resolve the repository for pull request ${id}`, "NOT_FOUND", [
804
+ "Pass --repo <repo> explicitly",
805
+ ]);
806
+ }
807
+ const base = `_apis/git/repositories/${encodeURIComponent(repoName)}/pullrequests/${id}/threads`;
808
+ const threadId = flagNumber(args, "thread");
809
+ if (threadId !== undefined) {
810
+ const comment = await request(profile, {
811
+ method: "POST",
812
+ path: `${base}/${threadId}/comments`,
813
+ project,
814
+ body: { content: body, commentType: "text" },
815
+ });
816
+ return { comment: { "pull-request": id, thread: threadId, id: comment.id ?? "", posted: true } };
817
+ }
818
+ const line = flagNumber(args, "line");
819
+ const thread = await request(profile, {
820
+ method: "POST",
821
+ path: base,
822
+ project,
823
+ body: {
824
+ comments: [{ parentCommentId: 0, content: body, commentType: "text" }],
825
+ status: "active",
826
+ threadContext: file
827
+ ? {
828
+ filePath: file,
829
+ rightFileStart: { line: line ?? 1, offset: 1 },
830
+ rightFileEnd: { line: line ?? 1, offset: 1 },
831
+ }
832
+ : undefined,
833
+ },
834
+ });
835
+ return {
836
+ comment: { "pull-request": id, thread: thread.id ?? "", file: file ?? "", posted: true },
837
+ help: [`Run \`ado-axi pr get ${id} --threads\` to see the discussion`],
838
+ };
839
+ }
840
+ const THREAD_STATUSES = {
841
+ active: "active",
842
+ fixed: "fixed",
843
+ wontfix: "wontFix",
844
+ "wont-fix": "wontFix",
845
+ closed: "closed",
846
+ bydesign: "byDesign",
847
+ "by-design": "byDesign",
848
+ pending: "pending",
849
+ };
850
+ function normalizeThreadStatus(value) {
851
+ const status = THREAD_STATUSES[value.toLowerCase()];
852
+ if (!status) {
853
+ throw new AxiError(`unknown thread status '${value}'`, "VALIDATION_ERROR", [
854
+ `Valid values: ${Object.keys(THREAD_STATUSES).join(", ")}`,
855
+ ]);
856
+ }
857
+ return status;
858
+ }
859
+ function requireThreadId(args, usage) {
860
+ const raw = flagNumber(args, "thread") ?? Number(args.positionals[1]);
861
+ if (raw === undefined || !Number.isInteger(raw) || raw < 1) {
862
+ throw new AxiError("--thread <id> is required", "VALIDATION_ERROR", [
863
+ `Usage: ${usage}`,
864
+ "Run `ado-axi pr thread list <id>` to find thread ids",
865
+ ]);
866
+ }
867
+ return raw;
868
+ }
869
+ function threadPath(repo, prId) {
870
+ return `_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads`;
871
+ }
872
+ function isSystemThread(thread) {
873
+ return !(thread.comments ?? []).some((c) => c.commentType !== "system");
874
+ }
875
+ async function resolvePrRepo(profile, id, args, project) {
876
+ const pr = await fetchPr(profile, id, flagString(args, "repo"), project);
877
+ const repo = pr.repository?.name ?? flagString(args, "repo");
878
+ if (!repo) {
879
+ throw new AxiError(`could not resolve the repository for pull request ${id}`, "NOT_FOUND", [
880
+ "Pass --repo <repo> explicitly",
881
+ ]);
882
+ }
883
+ return repo;
884
+ }
885
+ export async function threadPr(args) {
886
+ const first = args.positionals[0] ?? "list";
887
+ const sub = /^\d+$/.test(first) ? "list" : first;
888
+ const rest = sub === first ? { ...args, positionals: args.positionals.slice(1) } : args;
889
+ switch (sub) {
890
+ case "list":
891
+ return listThreads(rest);
892
+ case "resolve":
893
+ return setThreadStatus(rest, "fixed");
894
+ case "reopen":
895
+ return setThreadStatus(rest, "active");
896
+ case "reply":
897
+ return replyToThread(rest);
898
+ default:
899
+ throw new AxiError(`unknown subcommand \`pr thread ${sub}\``, "VALIDATION_ERROR", [
900
+ "Subcommands: list | resolve | reply | reopen",
901
+ "Run `ado-axi pr --help` for the full reference",
902
+ ]);
903
+ }
904
+ }
905
+ async function listThreads(args) {
906
+ assertKnownFlags(args, THREAD_LIST_FLAGS, "pr thread list");
907
+ const profile = profileFromArgs(args);
908
+ const project = requireProject(profile, "pr thread list");
909
+ const id = requirePrId(args);
910
+ const limit = flagNumber(args, "limit") ?? DEFAULT_SUMMARY_LIMIT;
911
+ const full = flagBool(args, "full");
912
+ const repo = await resolvePrRepo(profile, id, args, project);
913
+ const response = await request(profile, {
914
+ path: threadPath(repo, id),
915
+ project,
916
+ });
917
+ const threads = (response.value ?? []).filter((t) => !t.isDeleted && !isSystemThread(t));
918
+ if (threads.length === 0) {
919
+ return {
920
+ threads: `0 review comment threads on pull request #${id}`,
921
+ help: [`Run \`ado-axi pr comment ${id} --body "..."\` to start one`],
922
+ };
923
+ }
924
+ const unresolved = threads.filter((t) => (t.status ?? "active") === "active" || (t.status ?? "") === "pending");
925
+ const shown = threads.slice(0, limit);
926
+ const commentLimit = full ? Number.MAX_SAFE_INTEGER : 200;
927
+ const out = {
928
+ "pull-request": id,
929
+ repo,
930
+ count: countLine(shown.length, threads.length, "threads"),
931
+ unresolved: unresolved.length,
932
+ threads: pickFields(shown.map((t) => {
933
+ const comments = (t.comments ?? []).filter((c) => c.commentType !== "system");
934
+ const last = comments[comments.length - 1];
935
+ return {
936
+ id: t.id,
937
+ status: t.status ?? "",
938
+ file: t.threadContext?.filePath ?? "",
939
+ line: t.threadContext?.rightFileStart?.line ?? "",
940
+ comments: comments.length,
941
+ last: last
942
+ ? `${personName(last.author)}: ${truncate(htmlToText(last.content ?? ""), commentLimit).text}`
943
+ : "",
944
+ };
945
+ }), flagList(args, "fields")),
946
+ };
947
+ const help = [];
948
+ if (unresolved.length > 0) {
949
+ help.push(`Run \`ado-axi pr thread reply ${id} --thread <id> --body "..." --resolve\` to answer and close one`);
950
+ }
951
+ if (!full)
952
+ help.push(`Run \`ado-axi pr get ${id} --threads --full\` for complete comment text`);
953
+ out.help = help;
954
+ return out;
955
+ }
956
+ async function fetchThread(profile, repo, prId, threadId, project) {
957
+ return request(profile, { path: `${threadPath(repo, prId)}/${threadId}`, project });
958
+ }
959
+ async function setThreadStatus(args, fallback) {
960
+ assertKnownFlags(args, THREAD_STATUS_FLAGS, "pr thread resolve");
961
+ const profile = profileFromArgs(args);
962
+ const project = requireProject(profile, "pr thread resolve");
963
+ const id = requirePrId(args);
964
+ const threadId = requireThreadId(args, `ado-axi pr thread resolve ${id} --thread <id>`);
965
+ const status = normalizeThreadStatus(flagString(args, "status") ?? fallback);
966
+ const repo = await resolvePrRepo(profile, id, args, project);
967
+ const current = await fetchThread(profile, repo, id, threadId, project);
968
+ if ((current.status ?? "active") === status) {
969
+ return {
970
+ thread: `#${id} thread ${threadId} is already ${status} (no-op)`,
971
+ status,
972
+ };
973
+ }
974
+ const updated = await request(profile, {
975
+ method: "PATCH",
976
+ path: `${threadPath(repo, id)}/${threadId}`,
977
+ project,
978
+ body: { status },
979
+ });
980
+ return {
981
+ thread: {
982
+ "pull-request": id,
983
+ id: threadId,
984
+ status: updated.status ?? status,
985
+ previous: current.status ?? "active",
986
+ },
987
+ };
988
+ }
989
+ async function replyToThread(args) {
990
+ assertKnownFlags(args, THREAD_REPLY_FLAGS, "pr thread reply");
991
+ const profile = profileFromArgs(args);
992
+ const project = requireProject(profile, "pr thread reply");
993
+ const id = requirePrId(args);
994
+ const threadId = requireThreadId(args, `ado-axi pr thread reply ${id} --thread <id> --body "..."`);
995
+ const body = flagString(args, "body") ?? (await readStdinIfPiped())?.toString("utf8");
996
+ if (!body) {
997
+ throw new AxiError("--body is required", "VALIDATION_ERROR", [
998
+ `Usage: ado-axi pr thread reply ${id} --thread ${threadId} --body "..." [--resolve]`,
999
+ "Pipe the reply to stdin when omitting --body",
1000
+ ]);
1001
+ }
1002
+ const resolve = flagBool(args, "resolve");
1003
+ const status = resolve ? normalizeThreadStatus(flagString(args, "status") ?? "fixed") : undefined;
1004
+ const repo = await resolvePrRepo(profile, id, args, project);
1005
+ const comment = await request(profile, {
1006
+ method: "POST",
1007
+ path: `${threadPath(repo, id)}/${threadId}/comments`,
1008
+ project,
1009
+ body: { content: body, commentType: "text" },
1010
+ });
1011
+ const out = {
1012
+ reply: { "pull-request": id, thread: threadId, comment: comment.id ?? "", posted: true },
1013
+ };
1014
+ if (status) {
1015
+ const updated = await request(profile, {
1016
+ method: "PATCH",
1017
+ path: `${threadPath(repo, id)}/${threadId}`,
1018
+ project,
1019
+ body: { status },
1020
+ });
1021
+ out.reply.status = updated.status ?? status;
1022
+ }
1023
+ else {
1024
+ out.help = [`Run \`ado-axi pr thread resolve ${id} --thread ${threadId}\` to close the thread`];
1025
+ }
1026
+ return out;
1027
+ }
1028
+ //# sourceMappingURL=pr.js.map