task-while 0.0.1
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/LICENSE +21 -0
- package/README.md +322 -0
- package/bin/task-while.mjs +22 -0
- package/package.json +72 -0
- package/src/agents/claude.ts +175 -0
- package/src/agents/codex.ts +231 -0
- package/src/agents/provider-options.ts +45 -0
- package/src/agents/types.ts +69 -0
- package/src/batch/config.ts +109 -0
- package/src/batch/discovery.ts +35 -0
- package/src/batch/provider.ts +79 -0
- package/src/commands/batch.ts +266 -0
- package/src/commands/run.ts +270 -0
- package/src/core/engine-helpers.ts +114 -0
- package/src/core/engine-outcomes.ts +166 -0
- package/src/core/engine.ts +223 -0
- package/src/core/orchestrator-helpers.ts +52 -0
- package/src/core/orchestrator-integrate-resume.ts +149 -0
- package/src/core/orchestrator-review-resume.ts +228 -0
- package/src/core/orchestrator-task-attempt.ts +257 -0
- package/src/core/orchestrator.ts +99 -0
- package/src/core/runtime.ts +175 -0
- package/src/core/task-topology.ts +85 -0
- package/src/index.ts +121 -0
- package/src/prompts/implementer.ts +18 -0
- package/src/prompts/reviewer.ts +26 -0
- package/src/runtime/fs-runtime.ts +209 -0
- package/src/runtime/git.ts +137 -0
- package/src/runtime/github-pr-snapshot-decode.ts +307 -0
- package/src/runtime/github-pr-snapshot-queries.ts +137 -0
- package/src/runtime/github-pr-snapshot.ts +139 -0
- package/src/runtime/github.ts +232 -0
- package/src/runtime/path-layout.ts +13 -0
- package/src/runtime/workspace-resolver.ts +125 -0
- package/src/schema/index.ts +127 -0
- package/src/schema/model.ts +233 -0
- package/src/schema/shared.ts +93 -0
- package/src/task-sources/openspec/cli-json.ts +79 -0
- package/src/task-sources/openspec/context-files.ts +121 -0
- package/src/task-sources/openspec/parse-tasks-md.ts +57 -0
- package/src/task-sources/openspec/session.ts +235 -0
- package/src/task-sources/openspec/source.ts +59 -0
- package/src/task-sources/registry.ts +22 -0
- package/src/task-sources/spec-kit/parse-tasks-md.ts +48 -0
- package/src/task-sources/spec-kit/session.ts +174 -0
- package/src/task-sources/spec-kit/source.ts +30 -0
- package/src/task-sources/types.ts +47 -0
- package/src/types.ts +29 -0
- package/src/utils/fs.ts +31 -0
- package/src/workflow/config.ts +127 -0
- package/src/workflow/direct-preset.ts +44 -0
- package/src/workflow/finalize-task-checkbox.ts +24 -0
- package/src/workflow/preset.ts +86 -0
- package/src/workflow/pull-request-preset.ts +312 -0
- package/src/workflow/remote-reviewer.ts +243 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { execa } from 'execa'
|
|
4
|
+
|
|
5
|
+
import { filterPorcelainStatus } from '../utils/fs'
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
GitCheckoutBranchOptions,
|
|
9
|
+
GitCommitTaskInput,
|
|
10
|
+
GitCommitTaskResult,
|
|
11
|
+
GitPort,
|
|
12
|
+
GitPushBranchOptions,
|
|
13
|
+
} from '../core/runtime'
|
|
14
|
+
|
|
15
|
+
function normalizeRelativePath(value: string) {
|
|
16
|
+
return value.split(path.sep).join('/')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function runGit(cwd: string, args: string[]) {
|
|
20
|
+
const result = await execa('git', args, { cwd })
|
|
21
|
+
return result.stdout.trim()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class GitRuntime implements GitPort {
|
|
25
|
+
private readonly runtimeDirRelative: string
|
|
26
|
+
|
|
27
|
+
public constructor(
|
|
28
|
+
private readonly workspaceRoot: string,
|
|
29
|
+
runtimeDir: string,
|
|
30
|
+
) {
|
|
31
|
+
this.runtimeDirRelative = normalizeRelativePath(
|
|
32
|
+
path.relative(this.workspaceRoot, runtimeDir),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async checkoutBranch(
|
|
37
|
+
name: string,
|
|
38
|
+
options?: GitCheckoutBranchOptions,
|
|
39
|
+
) {
|
|
40
|
+
if (options?.create) {
|
|
41
|
+
const args = ['checkout', '-b', name]
|
|
42
|
+
if (options.startPoint) {
|
|
43
|
+
args.push(options.startPoint)
|
|
44
|
+
}
|
|
45
|
+
await runGit(this.workspaceRoot, args)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
await runGit(this.workspaceRoot, ['checkout', name])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async checkoutRemoteBranch(name: string) {
|
|
52
|
+
await runGit(this.workspaceRoot, ['fetch', 'origin', name])
|
|
53
|
+
await runGit(this.workspaceRoot, ['checkout', '-B', name, 'FETCH_HEAD'])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async commitTask(
|
|
57
|
+
input: GitCommitTaskInput,
|
|
58
|
+
): Promise<GitCommitTaskResult> {
|
|
59
|
+
await runGit(this.workspaceRoot, ['add', '-A', '.'])
|
|
60
|
+
await runGit(this.workspaceRoot, ['reset', '--', this.runtimeDirRelative])
|
|
61
|
+
await runGit(this.workspaceRoot, [
|
|
62
|
+
'commit',
|
|
63
|
+
'--allow-empty',
|
|
64
|
+
'-m',
|
|
65
|
+
input.message,
|
|
66
|
+
])
|
|
67
|
+
const commitSha = await runGit(this.workspaceRoot, ['rev-parse', 'HEAD'])
|
|
68
|
+
return { commitSha }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async deleteLocalBranch(name: string) {
|
|
72
|
+
await runGit(this.workspaceRoot, ['branch', '-D', name])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async getChangedFilesSinceHead() {
|
|
76
|
+
const [changed, untracked] = await Promise.all([
|
|
77
|
+
runGit(this.workspaceRoot, ['diff', '--name-only', 'HEAD']),
|
|
78
|
+
runGit(this.workspaceRoot, [
|
|
79
|
+
'ls-files',
|
|
80
|
+
'--others',
|
|
81
|
+
'--exclude-standard',
|
|
82
|
+
]),
|
|
83
|
+
])
|
|
84
|
+
const files = new Set(
|
|
85
|
+
[...changed.split('\n'), ...untracked.split('\n')]
|
|
86
|
+
.map((item) => item.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.filter(
|
|
89
|
+
(item) =>
|
|
90
|
+
item !== this.runtimeDirRelative &&
|
|
91
|
+
!item.startsWith(`${this.runtimeDirRelative}/`),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
return [...files].sort()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public async getCurrentBranch() {
|
|
98
|
+
return runGit(this.workspaceRoot, ['branch', '--show-current'])
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public async getHeadSha() {
|
|
102
|
+
return runGit(this.workspaceRoot, ['rev-parse', 'HEAD'])
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public async getHeadSubject() {
|
|
106
|
+
return runGit(this.workspaceRoot, ['log', '-1', '--format=%s', 'HEAD'])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async getHeadTimestamp() {
|
|
110
|
+
return runGit(this.workspaceRoot, ['log', '-1', '--format=%cI', 'HEAD'])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public async pullFastForward(branch: string) {
|
|
114
|
+
await runGit(this.workspaceRoot, ['pull', '--ff-only', 'origin', branch])
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public async pushBranch(name: string, options?: GitPushBranchOptions) {
|
|
118
|
+
const args = ['push']
|
|
119
|
+
if (options?.setUpstream) {
|
|
120
|
+
args.push('-u')
|
|
121
|
+
}
|
|
122
|
+
args.push('origin', name)
|
|
123
|
+
await runGit(this.workspaceRoot, args)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public async requireCleanWorktree() {
|
|
127
|
+
const status = await runGit(this.workspaceRoot, ['status', '--porcelain'])
|
|
128
|
+
const relevantLines = filterPorcelainStatus(
|
|
129
|
+
status.split('\n').filter(Boolean),
|
|
130
|
+
this.runtimeDirRelative,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if (relevantLines.length !== 0) {
|
|
134
|
+
throw new Error('Worktree must be clean before running task-while')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PullRequestDiscussionComment,
|
|
3
|
+
PullRequestReaction,
|
|
4
|
+
PullRequestReviewSummary,
|
|
5
|
+
PullRequestReviewThreadComment,
|
|
6
|
+
} from '../core/runtime'
|
|
7
|
+
|
|
8
|
+
export interface ConnectionPage<T> {
|
|
9
|
+
nodes: T[]
|
|
10
|
+
pageInfo: ConnectionPageInfo
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ConnectionPageInfo {
|
|
14
|
+
endCursor: null | string
|
|
15
|
+
hasNextPage: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PullRequestSnapshotPage {
|
|
19
|
+
comments: ConnectionPage<PullRequestDiscussionComment> | null
|
|
20
|
+
files: ConnectionPage<string> | null
|
|
21
|
+
reactions: ConnectionPage<PullRequestReaction> | null
|
|
22
|
+
reviews: ConnectionPage<PullRequestReviewSummary> | null
|
|
23
|
+
reviewThreads: ConnectionPage<ReviewThreadPageNode> | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReviewThreadPageNode {
|
|
27
|
+
comments: PullRequestReviewThreadComment[]
|
|
28
|
+
commentsPageInfo: ConnectionPageInfo
|
|
29
|
+
id: string
|
|
30
|
+
isOutdated: boolean
|
|
31
|
+
isResolved: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RequestedSnapshotConnections {
|
|
35
|
+
comments: boolean
|
|
36
|
+
files: boolean
|
|
37
|
+
reactions: boolean
|
|
38
|
+
reviews: boolean
|
|
39
|
+
reviewThreads: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ParseRequestedConnectionInput<T> {
|
|
43
|
+
label: string
|
|
44
|
+
mapNode: (node: Record<string, unknown>) => T
|
|
45
|
+
requested: boolean
|
|
46
|
+
value: unknown
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function asBoolean(value: unknown) {
|
|
50
|
+
return value === true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function asNullableNumber(value: unknown) {
|
|
54
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function asNullableString(value: unknown) {
|
|
58
|
+
return typeof value === 'string' ? value : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function asNumericId(value: unknown) {
|
|
62
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
63
|
+
return value
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
66
|
+
const parsed = Number(value)
|
|
67
|
+
return Number.isFinite(parsed) ? parsed : 0
|
|
68
|
+
}
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function asRecord(value: unknown) {
|
|
73
|
+
return value && typeof value === 'object'
|
|
74
|
+
? (value as Record<string, unknown>)
|
|
75
|
+
: null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function asString(value: unknown) {
|
|
79
|
+
return typeof value === 'string' ? value : ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface GraphQLReviewThreadCommentsNode {
|
|
83
|
+
comments?: Record<string, unknown>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface GraphQLReviewThreadCommentsData {
|
|
87
|
+
node?: GraphQLReviewThreadCommentsNode
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface GraphQLReviewThreadCommentsPayload {
|
|
91
|
+
data?: GraphQLReviewThreadCommentsData
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface GraphQLRepositoryPullRequestPayload {
|
|
95
|
+
pullRequest?: Record<string, unknown>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface GraphQLSnapshotRepository {
|
|
99
|
+
repository?: GraphQLRepositoryPullRequestPayload
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface GraphQLSnapshotPayload {
|
|
103
|
+
data?: GraphQLSnapshotRepository
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface LoginLike {
|
|
107
|
+
login?: unknown
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function invalidGraphQLError(label: string) {
|
|
111
|
+
return new Error(`Invalid GraphQL connection: ${label}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function missingGraphQLError(label: string) {
|
|
115
|
+
return new Error(`Missing GraphQL connection: ${label}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function requirePageInfo(value: unknown, label: string) {
|
|
119
|
+
const record = asRecord(value)
|
|
120
|
+
if (!record) {
|
|
121
|
+
throw invalidGraphQLError(`${label}.pageInfo`)
|
|
122
|
+
}
|
|
123
|
+
if (record.endCursor !== null && typeof record.endCursor !== 'string') {
|
|
124
|
+
throw invalidGraphQLError(`${label}.pageInfo.endCursor`)
|
|
125
|
+
}
|
|
126
|
+
if (typeof record.hasNextPage !== 'boolean') {
|
|
127
|
+
throw invalidGraphQLError(`${label}.pageInfo.hasNextPage`)
|
|
128
|
+
}
|
|
129
|
+
if (record.hasNextPage && typeof record.endCursor !== 'string') {
|
|
130
|
+
throw invalidGraphQLError(`${label}.pageInfo.endCursor`)
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
endCursor: record.endCursor,
|
|
134
|
+
hasNextPage: record.hasNextPage,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function requireConnectionRecord(value: unknown, label: string) {
|
|
139
|
+
const record = asRecord(value)
|
|
140
|
+
if (!record) {
|
|
141
|
+
throw missingGraphQLError(label)
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(record.nodes)) {
|
|
144
|
+
throw invalidGraphQLError(`${label}.nodes`)
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
nodes: record.nodes as Record<string, unknown>[],
|
|
148
|
+
pageInfo: requirePageInfo(record.pageInfo, label),
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseConnection<T>(
|
|
153
|
+
value: unknown,
|
|
154
|
+
label: string,
|
|
155
|
+
mapNode: (node: Record<string, unknown>) => T,
|
|
156
|
+
): ConnectionPage<T> {
|
|
157
|
+
const record = requireConnectionRecord(value, label)
|
|
158
|
+
return {
|
|
159
|
+
nodes: record.nodes.map((node) => mapNode(node)),
|
|
160
|
+
pageInfo: record.pageInfo,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseRequestedConnection<T>(
|
|
165
|
+
input: ParseRequestedConnectionInput<T>,
|
|
166
|
+
): ConnectionPage<T> | null {
|
|
167
|
+
if (!input.requested) {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
return parseConnection(input.value, input.label, input.mapNode)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function parseReviewThreadCommentsPage(
|
|
174
|
+
raw: string,
|
|
175
|
+
): ConnectionPage<PullRequestReviewThreadComment> {
|
|
176
|
+
const payload = JSON.parse(raw) as GraphQLReviewThreadCommentsPayload
|
|
177
|
+
return parseConnection(
|
|
178
|
+
payload.data?.node?.comments,
|
|
179
|
+
'reviewThread.comments',
|
|
180
|
+
(node) => toReviewThreadComment(node),
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function parseSnapshotPage(
|
|
185
|
+
raw: string,
|
|
186
|
+
requested: RequestedSnapshotConnections,
|
|
187
|
+
): PullRequestSnapshotPage {
|
|
188
|
+
const payload = JSON.parse(raw) as GraphQLSnapshotPayload
|
|
189
|
+
const repository = asRecord(payload.data?.repository)
|
|
190
|
+
const record = asRecord(repository?.pullRequest)
|
|
191
|
+
if (!record) {
|
|
192
|
+
throw new Error('Missing GraphQL node: repository.pullRequest')
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
comments: parseRequestedConnection({
|
|
196
|
+
label: 'comments',
|
|
197
|
+
requested: requested.comments,
|
|
198
|
+
value: record.comments,
|
|
199
|
+
mapNode: (node) => toDiscussionComment(node),
|
|
200
|
+
}),
|
|
201
|
+
files: parseRequestedConnection({
|
|
202
|
+
label: 'files',
|
|
203
|
+
requested: requested.files,
|
|
204
|
+
value: record.files,
|
|
205
|
+
mapNode: (node) => asString(node.path),
|
|
206
|
+
}),
|
|
207
|
+
reactions: parseRequestedConnection({
|
|
208
|
+
label: 'reactions',
|
|
209
|
+
requested: requested.reactions,
|
|
210
|
+
value: record.reactions,
|
|
211
|
+
mapNode: (node) => toReaction(node),
|
|
212
|
+
}),
|
|
213
|
+
reviews: parseRequestedConnection({
|
|
214
|
+
label: 'reviews',
|
|
215
|
+
requested: requested.reviews,
|
|
216
|
+
value: record.reviews,
|
|
217
|
+
mapNode: (node) => toReviewSummary(node),
|
|
218
|
+
}),
|
|
219
|
+
reviewThreads: parseRequestedConnection({
|
|
220
|
+
label: 'reviewThreads',
|
|
221
|
+
requested: requested.reviewThreads,
|
|
222
|
+
value: record.reviewThreads,
|
|
223
|
+
mapNode: (node) => toReviewThreadPageNode(node),
|
|
224
|
+
}),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function toDiscussionComment(
|
|
229
|
+
item: Record<string, unknown>,
|
|
230
|
+
): PullRequestDiscussionComment {
|
|
231
|
+
return {
|
|
232
|
+
id: asNumericId(item.id ?? item.databaseId),
|
|
233
|
+
body: asString(item.body),
|
|
234
|
+
createdAt: asString(item.created_at ?? item.createdAt),
|
|
235
|
+
url: asString(item.html_url ?? item.url),
|
|
236
|
+
userLogin: asString(
|
|
237
|
+
(item.user as LoginLike | null)?.login ??
|
|
238
|
+
(item.author as LoginLike | null)?.login,
|
|
239
|
+
),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function toReaction(item: Record<string, unknown>): PullRequestReaction {
|
|
244
|
+
return {
|
|
245
|
+
content: toReactionContent(asString(item.content)),
|
|
246
|
+
createdAt: asString(item.created_at ?? item.createdAt),
|
|
247
|
+
userLogin: asString((item.user as LoginLike | null)?.login),
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function toReactionContent(value: string) {
|
|
252
|
+
switch (value) {
|
|
253
|
+
case 'THUMBS_DOWN':
|
|
254
|
+
return '-1'
|
|
255
|
+
case 'THUMBS_UP':
|
|
256
|
+
return '+1'
|
|
257
|
+
default:
|
|
258
|
+
return value.toLowerCase()
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function toReviewSummary(
|
|
263
|
+
item: Record<string, unknown>,
|
|
264
|
+
): PullRequestReviewSummary {
|
|
265
|
+
return {
|
|
266
|
+
id: asNumericId(item.id ?? item.fullDatabaseId),
|
|
267
|
+
body: asString(item.body),
|
|
268
|
+
state: asString(item.state),
|
|
269
|
+
submittedAt: asNullableString(item.submitted_at ?? item.submittedAt),
|
|
270
|
+
url: asString(item.html_url ?? item.url),
|
|
271
|
+
userLogin: asString(
|
|
272
|
+
(item.user as LoginLike | null)?.login ??
|
|
273
|
+
(item.author as LoginLike | null)?.login,
|
|
274
|
+
),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function toReviewThreadComment(
|
|
279
|
+
item: Record<string, unknown>,
|
|
280
|
+
): PullRequestReviewThreadComment {
|
|
281
|
+
return {
|
|
282
|
+
body: asString(item.body),
|
|
283
|
+
createdAt: asString(item.createdAt),
|
|
284
|
+
line: asNullableNumber(item.line),
|
|
285
|
+
path: asString(item.path),
|
|
286
|
+
url: asString(item.url),
|
|
287
|
+
userLogin: asString((item.author as LoginLike | null)?.login),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function toReviewThreadPageNode(
|
|
292
|
+
item: Record<string, unknown>,
|
|
293
|
+
): ReviewThreadPageNode {
|
|
294
|
+
const commentsConnection = requireConnectionRecord(
|
|
295
|
+
item.comments,
|
|
296
|
+
'reviewThreads.comments',
|
|
297
|
+
)
|
|
298
|
+
return {
|
|
299
|
+
id: asString(item.id),
|
|
300
|
+
commentsPageInfo: commentsConnection.pageInfo,
|
|
301
|
+
isOutdated: asBoolean(item.isOutdated),
|
|
302
|
+
isResolved: asBoolean(item.isResolved),
|
|
303
|
+
comments: commentsConnection.nodes.map((comment) =>
|
|
304
|
+
toReviewThreadComment(comment),
|
|
305
|
+
),
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export interface BuildReviewThreadCommentsArgsInput {
|
|
2
|
+
after?: null | string
|
|
3
|
+
threadId: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface BuildSnapshotArgsInput {
|
|
7
|
+
commentsAfter?: null | string
|
|
8
|
+
filesAfter?: null | string
|
|
9
|
+
includeComments: boolean
|
|
10
|
+
includeFiles: boolean
|
|
11
|
+
includeReactions: boolean
|
|
12
|
+
includeReviews: boolean
|
|
13
|
+
includeReviewThreads: boolean
|
|
14
|
+
number: number
|
|
15
|
+
owner: string
|
|
16
|
+
reactionsAfter?: null | string
|
|
17
|
+
repo: string
|
|
18
|
+
reviewsAfter?: null | string
|
|
19
|
+
reviewThreadsAfter?: null | string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildReviewThreadCommentsArgs(
|
|
23
|
+
input: BuildReviewThreadCommentsArgsInput,
|
|
24
|
+
) {
|
|
25
|
+
const args = ['api', 'graphql', '-F', `threadId=${input.threadId}`]
|
|
26
|
+
if (input.after) {
|
|
27
|
+
args.push('-f', `commentsAfter=${input.after}`)
|
|
28
|
+
}
|
|
29
|
+
args.push(
|
|
30
|
+
'-f',
|
|
31
|
+
`query=${[
|
|
32
|
+
'query($threadId: ID!, $commentsAfter: String) {',
|
|
33
|
+
' node(id: $threadId) {',
|
|
34
|
+
' ... on PullRequestReviewThread {',
|
|
35
|
+
' comments(first: 100, after: $commentsAfter) {',
|
|
36
|
+
' pageInfo { hasNextPage endCursor }',
|
|
37
|
+
' nodes { author { login } body createdAt path line url }',
|
|
38
|
+
' }',
|
|
39
|
+
' }',
|
|
40
|
+
' }',
|
|
41
|
+
'}',
|
|
42
|
+
].join('\n')}`,
|
|
43
|
+
)
|
|
44
|
+
return args
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildSnapshotArgs(input: BuildSnapshotArgsInput) {
|
|
48
|
+
const args = [
|
|
49
|
+
'api',
|
|
50
|
+
'graphql',
|
|
51
|
+
'-f',
|
|
52
|
+
`owner=${input.owner}`,
|
|
53
|
+
'-f',
|
|
54
|
+
`repo=${input.repo}`,
|
|
55
|
+
'-F',
|
|
56
|
+
`number=${input.number}`,
|
|
57
|
+
'-F',
|
|
58
|
+
`includeFiles=${String(input.includeFiles)}`,
|
|
59
|
+
'-F',
|
|
60
|
+
`includeReactions=${String(input.includeReactions)}`,
|
|
61
|
+
'-F',
|
|
62
|
+
`includeReviews=${String(input.includeReviews)}`,
|
|
63
|
+
'-F',
|
|
64
|
+
`includeComments=${String(input.includeComments)}`,
|
|
65
|
+
'-F',
|
|
66
|
+
`includeReviewThreads=${String(input.includeReviewThreads)}`,
|
|
67
|
+
]
|
|
68
|
+
if (input.filesAfter) {
|
|
69
|
+
args.push('-f', `filesAfter=${input.filesAfter}`)
|
|
70
|
+
}
|
|
71
|
+
if (input.reactionsAfter) {
|
|
72
|
+
args.push('-f', `reactionsAfter=${input.reactionsAfter}`)
|
|
73
|
+
}
|
|
74
|
+
if (input.reviewsAfter) {
|
|
75
|
+
args.push('-f', `reviewsAfter=${input.reviewsAfter}`)
|
|
76
|
+
}
|
|
77
|
+
if (input.commentsAfter) {
|
|
78
|
+
args.push('-f', `commentsAfter=${input.commentsAfter}`)
|
|
79
|
+
}
|
|
80
|
+
if (input.reviewThreadsAfter) {
|
|
81
|
+
args.push('-f', `reviewThreadsAfter=${input.reviewThreadsAfter}`)
|
|
82
|
+
}
|
|
83
|
+
args.push(
|
|
84
|
+
'-f',
|
|
85
|
+
`query=${[
|
|
86
|
+
'query(',
|
|
87
|
+
' $owner: String!,',
|
|
88
|
+
' $repo: String!,',
|
|
89
|
+
' $number: Int!,',
|
|
90
|
+
' $includeFiles: Boolean!,',
|
|
91
|
+
' $includeReactions: Boolean!,',
|
|
92
|
+
' $includeReviews: Boolean!,',
|
|
93
|
+
' $includeComments: Boolean!,',
|
|
94
|
+
' $includeReviewThreads: Boolean!,',
|
|
95
|
+
' $filesAfter: String,',
|
|
96
|
+
' $reactionsAfter: String,',
|
|
97
|
+
' $reviewsAfter: String,',
|
|
98
|
+
' $commentsAfter: String,',
|
|
99
|
+
' $reviewThreadsAfter: String',
|
|
100
|
+
') {',
|
|
101
|
+
' repository(owner: $owner, name: $repo) {',
|
|
102
|
+
' pullRequest(number: $number) {',
|
|
103
|
+
' files(first: 100, after: $filesAfter) @include(if: $includeFiles) {',
|
|
104
|
+
' pageInfo { hasNextPage endCursor }',
|
|
105
|
+
' nodes { path }',
|
|
106
|
+
' }',
|
|
107
|
+
' reactions(first: 100, after: $reactionsAfter) @include(if: $includeReactions) {',
|
|
108
|
+
' pageInfo { hasNextPage endCursor }',
|
|
109
|
+
' nodes { content createdAt user { login } }',
|
|
110
|
+
' }',
|
|
111
|
+
' reviews(first: 100, after: $reviewsAfter) @include(if: $includeReviews) {',
|
|
112
|
+
' pageInfo { hasNextPage endCursor }',
|
|
113
|
+
' nodes { author { login } body fullDatabaseId state submittedAt url }',
|
|
114
|
+
' }',
|
|
115
|
+
' comments(first: 100, after: $commentsAfter) @include(if: $includeComments) {',
|
|
116
|
+
' pageInfo { hasNextPage endCursor }',
|
|
117
|
+
' nodes { author { login } body createdAt databaseId url }',
|
|
118
|
+
' }',
|
|
119
|
+
' reviewThreads(first: 100, after: $reviewThreadsAfter) @include(if: $includeReviewThreads) {',
|
|
120
|
+
' pageInfo { hasNextPage endCursor }',
|
|
121
|
+
' nodes {',
|
|
122
|
+
' id',
|
|
123
|
+
' isResolved',
|
|
124
|
+
' isOutdated',
|
|
125
|
+
' comments(first: 100) {',
|
|
126
|
+
' pageInfo { hasNextPage endCursor }',
|
|
127
|
+
' nodes { author { login } body createdAt path line url }',
|
|
128
|
+
' }',
|
|
129
|
+
' }',
|
|
130
|
+
' }',
|
|
131
|
+
' }',
|
|
132
|
+
' }',
|
|
133
|
+
'}',
|
|
134
|
+
].join('\n')}`,
|
|
135
|
+
)
|
|
136
|
+
return args
|
|
137
|
+
}
|