@kurokeita/add-skill 2.4.0 → 2.5.0
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.
- package/dist/bin/cli.js +1 -1
- package/dist/skills/statusline-setup/.claude-plugin/plugin.json +7 -0
- package/dist/skills/statusline-setup/SKILL.md +451 -15
- package/dist/skills/statusline-setup/mod/hooks/discover.ts +142 -0
- package/dist/skills/statusline-setup/mod/hooks/fetch.ts +327 -0
- package/dist/skills/statusline-setup/mod/hooks/hooks.json +1 -0
- package/dist/skills/statusline-setup/mod/hooks/register.tsx +411 -0
- package/dist/skills/statusline-setup/mod/tests/work.test.ts +306 -0
- package/dist/skills/statusline-setup/mod/types/index.d.ts +35 -0
- package/package.json +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { SessionMessage } from "claude-code"
|
|
2
|
+
|
|
3
|
+
import type { CheckState, Step } from "../types"
|
|
4
|
+
|
|
5
|
+
export type Platform = "github" | "azure"
|
|
6
|
+
|
|
7
|
+
export function platformOf(remote: string): Platform | null {
|
|
8
|
+
if (/github\.com[:/]/.test(remote)) return "github"
|
|
9
|
+
if (/dev\.azure\.com[:/]|\.visualstudio\.com\//.test(remote)) return "azure"
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function repoNameOf(remote: string): string {
|
|
14
|
+
return (
|
|
15
|
+
remote
|
|
16
|
+
.trim()
|
|
17
|
+
.replace(/\.git$/, "")
|
|
18
|
+
.split(/[/:]/)
|
|
19
|
+
.pop() ?? ""
|
|
20
|
+
).toLowerCase()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function decoded(name: string): string {
|
|
24
|
+
try {
|
|
25
|
+
return decodeURIComponent(name).toLowerCase()
|
|
26
|
+
} catch {
|
|
27
|
+
return name.toLowerCase()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// PR links anywhere in the conversation: a command's output (gh pr create and
|
|
32
|
+
// gh pr edit print the URL), the model's own text, or a link the person pasted.
|
|
33
|
+
// Only links into this repository count, since the fetch runs against it.
|
|
34
|
+
export function prNumbersIn(
|
|
35
|
+
messages: readonly SessionMessage[],
|
|
36
|
+
platform: Platform,
|
|
37
|
+
repo: string,
|
|
38
|
+
): number[] {
|
|
39
|
+
const pattern =
|
|
40
|
+
platform === "github"
|
|
41
|
+
? /github\.com\/[\w.-]+\/([\w.-]+)\/pull\/(\d{1,9})/g
|
|
42
|
+
: /_git\/([^/\s?#"']+)\/pullrequest\/(\d{1,9})/gi
|
|
43
|
+
const found = new Set<number>()
|
|
44
|
+
for (const message of messages) {
|
|
45
|
+
const texts = [message.text]
|
|
46
|
+
for (const use of message.toolUses)
|
|
47
|
+
texts.push(JSON.stringify(use.input), use.text ?? "")
|
|
48
|
+
for (const result of message.toolResults ?? []) texts.push(result.text)
|
|
49
|
+
for (const text of texts) {
|
|
50
|
+
for (const match of text.matchAll(pattern)) {
|
|
51
|
+
if (decoded(match[1] ?? "") === repo) found.add(Number(match[2]))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [...found]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Items a branch without a PR points at: a 3-9 digit segment of its name (plus
|
|
59
|
+
// gh issue develop's "<n>-slug"), then AB#N or #N in its own commits. 9 digits
|
|
60
|
+
// at most, as both APIs take 32-bit ids and one bad literal fails the query.
|
|
61
|
+
export function itemRefs(
|
|
62
|
+
platform: Platform,
|
|
63
|
+
branch: string,
|
|
64
|
+
commitLog: string,
|
|
65
|
+
): number[] {
|
|
66
|
+
const refs: string[] = []
|
|
67
|
+
const lead =
|
|
68
|
+
platform === "github" ? /^(\d{1,9})-/.exec(branch)?.[1] : undefined
|
|
69
|
+
if (lead) refs.push(lead)
|
|
70
|
+
refs.push(
|
|
71
|
+
...branch.split(/[^A-Za-z0-9]+/).filter((part) => /^\d{3,9}$/.test(part)),
|
|
72
|
+
)
|
|
73
|
+
const pattern = platform === "github" ? /#(\d{1,9})/g : /AB#(\d{1,9})/g
|
|
74
|
+
for (const match of commitLog.matchAll(pattern)) refs.push(match[1] ?? "")
|
|
75
|
+
return [...new Set(refs.filter(Boolean).map(Number))].slice(0, 10)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function githubChecks(rollup: string | null | undefined): CheckState {
|
|
79
|
+
if (rollup === "SUCCESS") return "passing"
|
|
80
|
+
if (rollup === "FAILURE" || rollup === "ERROR") return "failing"
|
|
81
|
+
if (rollup === "PENDING" || rollup === "EXPECTED") return "pending"
|
|
82
|
+
return "none"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function azureChecks(statuses: readonly string[]): CheckState {
|
|
86
|
+
if (statuses.some((s) => s === "rejected" || s === "broken")) return "failing"
|
|
87
|
+
if (statuses.some((s) => s === "running" || s === "queued")) return "pending"
|
|
88
|
+
if (statuses.some((s) => s === "approved")) return "passing"
|
|
89
|
+
return "none"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function azureReview(votes: readonly number[]): string {
|
|
93
|
+
if (votes.includes(-10)) return "rejected"
|
|
94
|
+
if (votes.includes(-5)) return "waiting for author"
|
|
95
|
+
if (votes.some((vote) => vote >= 5)) return "approved"
|
|
96
|
+
return ""
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const SUMMARY_SYSTEM = `You summarize a coding session for a side panel.
|
|
100
|
+
Reply with JSON only, no prose and no code fence:
|
|
101
|
+
{"goal": "<2-4 sentences: what the session is trying to achieve and where it stands now>",
|
|
102
|
+
"steps": [{"text": "<one short line>", "status": "done" | "current" | "todo"}]}
|
|
103
|
+
Steps run in order, at most 12, and at most one is "current".`
|
|
104
|
+
|
|
105
|
+
// The newest part of the conversation that fits the model's budget.
|
|
106
|
+
export function transcriptOf(messages: readonly SessionMessage[]): string {
|
|
107
|
+
const lines = messages.map((m) => {
|
|
108
|
+
const tools = m.toolUses.map(
|
|
109
|
+
(use) => `[${use.tool}] ${JSON.stringify(use.input).slice(0, 200)}`,
|
|
110
|
+
)
|
|
111
|
+
return `${m.role}: ${[m.text.slice(0, 2000), ...tools].filter(Boolean).join("\n")}`
|
|
112
|
+
})
|
|
113
|
+
return lines.join("\n\n").slice(-60_000)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseSummary(
|
|
117
|
+
text: string,
|
|
118
|
+
): { goal: string; steps: Step[] } | null {
|
|
119
|
+
const start = text.indexOf("{")
|
|
120
|
+
const end = text.lastIndexOf("}")
|
|
121
|
+
if (start < 0 || end < start) return null
|
|
122
|
+
try {
|
|
123
|
+
const value = JSON.parse(text.slice(start, end + 1)) as {
|
|
124
|
+
goal?: unknown
|
|
125
|
+
steps?: unknown
|
|
126
|
+
}
|
|
127
|
+
if (typeof value.goal !== "string" || !Array.isArray(value.steps))
|
|
128
|
+
return null
|
|
129
|
+
const steps: Step[] = []
|
|
130
|
+
for (const step of value.steps as { text?: unknown; status?: unknown }[]) {
|
|
131
|
+
if (typeof step?.text !== "string") continue
|
|
132
|
+
const status =
|
|
133
|
+
step.status === "done" || step.status === "current"
|
|
134
|
+
? step.status
|
|
135
|
+
: "todo"
|
|
136
|
+
steps.push({ text: step.text, status })
|
|
137
|
+
}
|
|
138
|
+
return { goal: value.goal, steps }
|
|
139
|
+
} catch {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { Item, Pr, Work } from "../types"
|
|
2
|
+
import {
|
|
3
|
+
azureChecks,
|
|
4
|
+
azureReview,
|
|
5
|
+
githubChecks,
|
|
6
|
+
itemRefs,
|
|
7
|
+
type Platform,
|
|
8
|
+
} from "./discover"
|
|
9
|
+
|
|
10
|
+
// Runs a command; register.tsx supplies it, since only that file may use $.
|
|
11
|
+
export type Run = (
|
|
12
|
+
argv: string[],
|
|
13
|
+
) => Promise<{ exitCode: number; stdout: string; stderr: string }>
|
|
14
|
+
|
|
15
|
+
export async function git(run: Run, args: string[]): Promise<string> {
|
|
16
|
+
const { exitCode, stdout } = await run(["git", ...args])
|
|
17
|
+
return exitCode === 0 ? stdout.trim() : ""
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function firstLine(text: string): string {
|
|
21
|
+
return text.trim().split("\n")[0] ?? ""
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function refsByBranch(
|
|
25
|
+
run: Run,
|
|
26
|
+
platform: Platform,
|
|
27
|
+
branches: readonly string[],
|
|
28
|
+
) {
|
|
29
|
+
const base = await git(run, [
|
|
30
|
+
"symbolic-ref",
|
|
31
|
+
"--short",
|
|
32
|
+
"refs/remotes/origin/HEAD",
|
|
33
|
+
])
|
|
34
|
+
const refs = new Map<string, number[]>()
|
|
35
|
+
for (const branch of branches) {
|
|
36
|
+
const log = base
|
|
37
|
+
? await git(run, ["log", "--format=%B", `${base}..${branch}`])
|
|
38
|
+
: ""
|
|
39
|
+
refs.set(branch, itemRefs(platform, branch, log))
|
|
40
|
+
}
|
|
41
|
+
return refs
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PR_FIELDS = `number title url state isDraft reviewDecision headRefName
|
|
45
|
+
comments { totalCount } reviewThreads { totalCount }
|
|
46
|
+
commits(last: 1) { nodes { commit { statusCheckRollup { state } } } }
|
|
47
|
+
closingIssuesReferences(first: 10) { nodes { number title url state } }`
|
|
48
|
+
|
|
49
|
+
type GhIssue = { number: number; title: string; url: string; state: string }
|
|
50
|
+
type GhPr = {
|
|
51
|
+
number: number
|
|
52
|
+
title: string
|
|
53
|
+
url: string
|
|
54
|
+
state: string
|
|
55
|
+
isDraft: boolean
|
|
56
|
+
reviewDecision: string | null
|
|
57
|
+
headRefName: string
|
|
58
|
+
comments: { totalCount: number }
|
|
59
|
+
reviewThreads: { totalCount: number }
|
|
60
|
+
commits: {
|
|
61
|
+
nodes: { commit: { statusCheckRollup: { state: string } | null } }[]
|
|
62
|
+
}
|
|
63
|
+
closingIssuesReferences: { nodes: GhIssue[] }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ghItem = (issue: GhIssue): Item => ({
|
|
67
|
+
id: issue.number,
|
|
68
|
+
url: issue.url,
|
|
69
|
+
title: issue.title,
|
|
70
|
+
state: issue.state.toLowerCase(),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// One GraphQL call: each branch's newest PR, each PR the conversation linked,
|
|
74
|
+
// and every item ref, as aliases. Branch names go in as variables, never into
|
|
75
|
+
// the query text. gh exits 1 when an alias does not resolve yet still prints
|
|
76
|
+
// the rest, so success is judged by data.repository.
|
|
77
|
+
export async function fetchGitHub(
|
|
78
|
+
run: Run,
|
|
79
|
+
branches: readonly string[],
|
|
80
|
+
prNumbers: readonly number[],
|
|
81
|
+
): Promise<Work> {
|
|
82
|
+
const refs = await refsByBranch(run, "github", branches)
|
|
83
|
+
const vars: string[] = []
|
|
84
|
+
const fields: string[] = []
|
|
85
|
+
const args: string[] = []
|
|
86
|
+
branches.forEach((branch, i) => {
|
|
87
|
+
vars.push(`, $b${i}: String!`)
|
|
88
|
+
args.push("-f", `b${i}=${branch}`)
|
|
89
|
+
fields.push(`b${i}: pullRequests(headRefName: $b${i}, first: 1, states: [OPEN, MERGED, CLOSED],
|
|
90
|
+
orderBy: {field: CREATED_AT, direction: DESC}) { nodes { ...P } }`)
|
|
91
|
+
})
|
|
92
|
+
for (const n of prNumbers)
|
|
93
|
+
fields.push(`p${n}: pullRequest(number: ${n}) { ...P }`)
|
|
94
|
+
for (const n of new Set([...refs.values()].flat())) {
|
|
95
|
+
fields.push(
|
|
96
|
+
`i${n}: issueOrPullRequest(number: ${n}) { ... on Issue { number title url state } }`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
if (fields.length === 0) return { prs: [], items: [], fetchedAt: Date.now() }
|
|
100
|
+
const query = `query($owner: String!, $name: String!${vars.join("")}) {
|
|
101
|
+
repository(owner: $owner, name: $name) { ${fields.join("\n")} }
|
|
102
|
+
} fragment P on PullRequest { ${PR_FIELDS} }`
|
|
103
|
+
const out = await run([
|
|
104
|
+
"gh",
|
|
105
|
+
"api",
|
|
106
|
+
"graphql",
|
|
107
|
+
"-F",
|
|
108
|
+
"owner={owner}",
|
|
109
|
+
"-F",
|
|
110
|
+
"name={repo}",
|
|
111
|
+
...args,
|
|
112
|
+
"-f",
|
|
113
|
+
`query=${query}`,
|
|
114
|
+
])
|
|
115
|
+
let repo: Record<string, unknown> | undefined
|
|
116
|
+
try {
|
|
117
|
+
repo = (
|
|
118
|
+
JSON.parse(out.stdout) as {
|
|
119
|
+
data?: { repository?: Record<string, unknown> }
|
|
120
|
+
}
|
|
121
|
+
).data?.repository
|
|
122
|
+
} catch {
|
|
123
|
+
repo = undefined
|
|
124
|
+
}
|
|
125
|
+
if (!repo)
|
|
126
|
+
return {
|
|
127
|
+
prs: [],
|
|
128
|
+
items: [],
|
|
129
|
+
fetchedAt: Date.now(),
|
|
130
|
+
error: `gh: ${firstLine(out.stderr) || "no data"}`,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const prs = new Map<number, Pr>()
|
|
134
|
+
const items = new Map<number, Item>()
|
|
135
|
+
const addPr = (pr: GhPr) => {
|
|
136
|
+
prs.set(pr.number, {
|
|
137
|
+
number: pr.number,
|
|
138
|
+
url: pr.url,
|
|
139
|
+
title: pr.title,
|
|
140
|
+
branch: pr.headRefName,
|
|
141
|
+
state:
|
|
142
|
+
pr.isDraft && pr.state === "OPEN" ? "draft" : pr.state.toLowerCase(),
|
|
143
|
+
review: (pr.reviewDecision ?? "").toLowerCase().replace(/_/g, " "),
|
|
144
|
+
checks: githubChecks(
|
|
145
|
+
pr.commits.nodes[0]?.commit.statusCheckRollup?.state,
|
|
146
|
+
),
|
|
147
|
+
comments: pr.comments.totalCount + pr.reviewThreads.totalCount,
|
|
148
|
+
})
|
|
149
|
+
for (const issue of pr.closingIssuesReferences.nodes)
|
|
150
|
+
items.set(issue.number, ghItem(issue))
|
|
151
|
+
}
|
|
152
|
+
branches.forEach((branch, i) => {
|
|
153
|
+
const pr = (repo[`b${i}`] as { nodes: GhPr[] } | null)?.nodes[0]
|
|
154
|
+
if (pr) {
|
|
155
|
+
addPr(pr)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
for (const n of refs.get(branch) ?? []) {
|
|
159
|
+
const issue = repo[`i${n}`] as GhIssue | null
|
|
160
|
+
if (issue?.number) items.set(issue.number, ghItem(issue))
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
for (const n of prNumbers) {
|
|
164
|
+
const pr = repo[`p${n}`] as GhPr | null
|
|
165
|
+
if (pr) addPr(pr)
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
prs: [...prs.values()],
|
|
169
|
+
items: [...items.values()],
|
|
170
|
+
fetchedAt: Date.now(),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function az(run: Run, args: string[]): Promise<unknown> {
|
|
175
|
+
const out = await run(["az", ...args, "--only-show-errors", "-o", "json"])
|
|
176
|
+
if (out.exitCode !== 0) return null
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(out.stdout)
|
|
179
|
+
} catch {
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type AzPr = {
|
|
185
|
+
pullRequestId: number
|
|
186
|
+
title: string
|
|
187
|
+
status: string
|
|
188
|
+
isDraft: boolean
|
|
189
|
+
url: string
|
|
190
|
+
sourceRefName: string
|
|
191
|
+
reviewers?: { vote: number }[]
|
|
192
|
+
repository: { id: string; project: { id: string } }
|
|
193
|
+
}
|
|
194
|
+
type AzWorkItem = { id: number; url: string; fields: Record<string, string> }
|
|
195
|
+
|
|
196
|
+
const AZ_STATE: Record<string, string> = {
|
|
197
|
+
active: "open",
|
|
198
|
+
completed: "merged",
|
|
199
|
+
abandoned: "closed",
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const azItem = (w: AzWorkItem): Item => ({
|
|
203
|
+
id: w.id,
|
|
204
|
+
url: w.url.replace("/_apis/wit/workItems/", "/_workitems/edit/"),
|
|
205
|
+
title: w.fields["System.Title"] ?? "",
|
|
206
|
+
state: (w.fields["System.State"] ?? "").toLowerCase(),
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// One az call per lookup, run in parallel: each branch's newest PR, each linked
|
|
210
|
+
// PR, then each PR's work items, blocking policies (its checks) and threads.
|
|
211
|
+
export async function fetchAzure(
|
|
212
|
+
run: Run,
|
|
213
|
+
branches: readonly string[],
|
|
214
|
+
prNumbers: readonly number[],
|
|
215
|
+
): Promise<Work> {
|
|
216
|
+
const refs = await refsByBranch(run, "azure", branches)
|
|
217
|
+
const byBranch = await Promise.all(
|
|
218
|
+
branches.map((b) =>
|
|
219
|
+
az(run, [
|
|
220
|
+
"repos",
|
|
221
|
+
"pr",
|
|
222
|
+
"list",
|
|
223
|
+
"--source-branch",
|
|
224
|
+
b,
|
|
225
|
+
"--status",
|
|
226
|
+
"all",
|
|
227
|
+
"--top",
|
|
228
|
+
"1",
|
|
229
|
+
]),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
const byNumber = await Promise.all(
|
|
233
|
+
prNumbers.map((n) => az(run, ["repos", "pr", "show", "--id", String(n)])),
|
|
234
|
+
)
|
|
235
|
+
if (
|
|
236
|
+
[...byBranch, ...byNumber].length > 0 &&
|
|
237
|
+
[...byBranch, ...byNumber].every((r) => r === null)
|
|
238
|
+
) {
|
|
239
|
+
return {
|
|
240
|
+
prs: [],
|
|
241
|
+
items: [],
|
|
242
|
+
fetchedAt: Date.now(),
|
|
243
|
+
error: "az: every lookup failed (logged in?)",
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const raw = new Map<number, AzPr>()
|
|
248
|
+
const refsWithoutPr: number[] = []
|
|
249
|
+
byBranch.forEach((list, i) => {
|
|
250
|
+
const pr = (list as AzPr[] | null)?.[0]
|
|
251
|
+
if (pr) raw.set(pr.pullRequestId, pr)
|
|
252
|
+
else refsWithoutPr.push(...(refs.get(branches[i] ?? "") ?? []))
|
|
253
|
+
})
|
|
254
|
+
for (const pr of byNumber as (AzPr | null)[])
|
|
255
|
+
if (pr) raw.set(pr.pullRequestId, pr)
|
|
256
|
+
|
|
257
|
+
const items = new Map<number, Item>()
|
|
258
|
+
const prs = await Promise.all(
|
|
259
|
+
[...raw.values()].map(async (pr): Promise<Pr> => {
|
|
260
|
+
const id = String(pr.pullRequestId)
|
|
261
|
+
const [workItems, policies, threads] = await Promise.all([
|
|
262
|
+
az(run, ["repos", "pr", "work-item", "list", "--id", id]),
|
|
263
|
+
az(run, ["repos", "pr", "policy", "list", "--id", id]),
|
|
264
|
+
az(run, [
|
|
265
|
+
"devops",
|
|
266
|
+
"invoke",
|
|
267
|
+
"--area",
|
|
268
|
+
"git",
|
|
269
|
+
"--resource",
|
|
270
|
+
"pullRequestThreads",
|
|
271
|
+
"--api-version",
|
|
272
|
+
"7.1",
|
|
273
|
+
"--route-parameters",
|
|
274
|
+
`project=${pr.repository.project.id}`,
|
|
275
|
+
`repositoryId=${pr.repository.id}`,
|
|
276
|
+
`pullRequestId=${id}`,
|
|
277
|
+
]),
|
|
278
|
+
])
|
|
279
|
+
for (const w of (workItems as AzWorkItem[] | null) ?? [])
|
|
280
|
+
items.set(w.id, azItem(w))
|
|
281
|
+
const blocking = (
|
|
282
|
+
(policies as
|
|
283
|
+
| { status: string; configuration: { isBlocking: boolean } }[]
|
|
284
|
+
| null) ?? []
|
|
285
|
+
)
|
|
286
|
+
.filter((p) => p.configuration.isBlocking)
|
|
287
|
+
.map((p) => p.status)
|
|
288
|
+
const comments = (
|
|
289
|
+
(
|
|
290
|
+
threads as {
|
|
291
|
+
value?: {
|
|
292
|
+
isDeleted: boolean
|
|
293
|
+
comments: { commentType: string }[]
|
|
294
|
+
}[]
|
|
295
|
+
} | null
|
|
296
|
+
)?.value ?? []
|
|
297
|
+
).filter(
|
|
298
|
+
(t) => !t.isDeleted && t.comments[0]?.commentType === "text",
|
|
299
|
+
).length
|
|
300
|
+
return {
|
|
301
|
+
number: pr.pullRequestId,
|
|
302
|
+
url: pr.url
|
|
303
|
+
.replace("/_apis/git/repositories/", "/_git/")
|
|
304
|
+
.replace("/pullRequests/", "/pullrequest/"),
|
|
305
|
+
title: pr.title,
|
|
306
|
+
branch: pr.sourceRefName.replace(/^refs\/heads\//, ""),
|
|
307
|
+
state:
|
|
308
|
+
pr.isDraft && pr.status === "active"
|
|
309
|
+
? "draft"
|
|
310
|
+
: (AZ_STATE[pr.status] ?? pr.status),
|
|
311
|
+
review: azureReview((pr.reviewers ?? []).map((r) => r.vote)),
|
|
312
|
+
checks: azureChecks(blocking),
|
|
313
|
+
comments,
|
|
314
|
+
}
|
|
315
|
+
}),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
const missing = [...new Set(refsWithoutPr)].filter((n) => !items.has(n))
|
|
319
|
+
if (missing.length > 0) {
|
|
320
|
+
const wiql = `SELECT [System.Id], [System.Title], [System.State] FROM WorkItems WHERE [System.Id] IN (${missing.join(", ")})`
|
|
321
|
+
for (const w of ((await az(run, ["boards", "query", "--wiql", wiql])) as
|
|
322
|
+
| AzWorkItem[]
|
|
323
|
+
| null) ?? [])
|
|
324
|
+
items.set(w.id, azItem(w))
|
|
325
|
+
}
|
|
326
|
+
return { prs, items: [...items.values()], fetchedAt: Date.now() }
|
|
327
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "modules": ["./register.tsx"] }
|