toolcraft 0.0.11 → 0.0.13
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/cli.js +274 -160
- package/dist/renderer.d.ts +8 -2
- package/dist/renderer.js +71 -12
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +132 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +116 -7
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +2 -2
- package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/text.js +8 -0
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -2
- package/node_modules/@poe-code/design-system/dist/index.js +2 -1
- package/node_modules/@poe-code/process-runner/README.md +41 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +2 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +40 -0
- package/node_modules/@poe-code/process-runner/dist/docker/context.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/docker/context.js +30 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.d.ts +28 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +428 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.d.ts +2 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +131 -0
- package/node_modules/@poe-code/process-runner/dist/docker/engine.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/docker/engine.js +24 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.d.ts +2 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +48 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +74 -0
- package/node_modules/@poe-code/process-runner/dist/index.d.ts +8 -0
- package/node_modules/@poe-code/process-runner/dist/index.js +7 -0
- package/node_modules/@poe-code/process-runner/dist/testing/index.d.ts +2 -0
- package/node_modules/@poe-code/process-runner/dist/testing/index.js +1 -0
- package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +115 -0
- package/node_modules/@poe-code/process-runner/dist/testing/verify.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/testing/verify.js +359 -0
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +180 -0
- package/node_modules/@poe-code/process-runner/dist/types.js +1 -0
- package/node_modules/@poe-code/process-runner/package.json +27 -0
- package/node_modules/@poe-code/task-list/README.md +49 -5
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.d.ts +19 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +62 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +13 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +627 -0
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +253 -41
- package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +7 -1
- package/node_modules/@poe-code/task-list/dist/backends/utils.js +21 -0
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +171 -16
- package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/task-list/dist/index.js +1 -1
- package/node_modules/@poe-code/task-list/dist/open.d.ts +4 -2
- package/node_modules/@poe-code/task-list/dist/open.js +27 -3
- package/node_modules/@poe-code/task-list/dist/types.d.ts +51 -3
- package/node_modules/@poe-code/task-list/dist/types.js +25 -0
- package/node_modules/@poe-code/task-list/package.json +1 -0
- package/package.json +11 -4
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { eventsFromState, findEvent } from "../state-machine.js";
|
|
2
|
+
import { AnchorNotFoundError, InvalidTransitionError, OrderMismatchError, TaskNotFoundError } from "../types.js";
|
|
3
|
+
import { createGhClient } from "./gh-issues-client.js";
|
|
4
|
+
import { applyOrder, sortTasks } from "./utils.js";
|
|
5
|
+
const PROJECT_ORGANIZATION_QUERY = `query Project($owner: String!, $number: Int!) {
|
|
6
|
+
organization(login: $owner) {
|
|
7
|
+
projectV2(number: $number) {
|
|
8
|
+
id
|
|
9
|
+
title
|
|
10
|
+
field(name: "Status") {
|
|
11
|
+
... on ProjectV2SingleSelectField {
|
|
12
|
+
id
|
|
13
|
+
options { id name }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}`;
|
|
19
|
+
const PROJECT_USER_QUERY = `query Project($owner: String!, $number: Int!) {
|
|
20
|
+
user(login: $owner) {
|
|
21
|
+
projectV2(number: $number) {
|
|
22
|
+
id
|
|
23
|
+
title
|
|
24
|
+
field(name: "Status") {
|
|
25
|
+
... on ProjectV2SingleSelectField {
|
|
26
|
+
id
|
|
27
|
+
options { id name }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}`;
|
|
33
|
+
const PROJECT_ITEMS_QUERY = `query Items($projectId: ID!, $after: String) {
|
|
34
|
+
node(id: $projectId) {
|
|
35
|
+
... on ProjectV2 {
|
|
36
|
+
items(first: 100, after: $after) {
|
|
37
|
+
nodes {
|
|
38
|
+
id
|
|
39
|
+
content {
|
|
40
|
+
__typename
|
|
41
|
+
... on Issue {
|
|
42
|
+
number
|
|
43
|
+
title
|
|
44
|
+
body
|
|
45
|
+
url
|
|
46
|
+
createdAt
|
|
47
|
+
labels(first: 50) { nodes { name } }
|
|
48
|
+
assignees(first: 20) { nodes { login } }
|
|
49
|
+
milestone { title }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
fieldValueByName(name: "Status") {
|
|
53
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
54
|
+
name
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
pageInfo { hasNextPage endCursor }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}`;
|
|
63
|
+
const ISSUE_QUERY = `query Issue($owner: String!, $repo: String!, $number: Int!) {
|
|
64
|
+
repository(owner: $owner, name: $repo) {
|
|
65
|
+
issue(number: $number) {
|
|
66
|
+
number
|
|
67
|
+
title
|
|
68
|
+
body
|
|
69
|
+
url
|
|
70
|
+
createdAt
|
|
71
|
+
labels(first: 50) { nodes { name } }
|
|
72
|
+
assignees(first: 20) { nodes { login } }
|
|
73
|
+
milestone { title }
|
|
74
|
+
projectItems(first: 10) {
|
|
75
|
+
nodes {
|
|
76
|
+
id
|
|
77
|
+
project { id }
|
|
78
|
+
fieldValueByName(name: "Status") {
|
|
79
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
80
|
+
name
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}`;
|
|
88
|
+
const REPOSITORY_QUERY = `query Repository($owner: String!, $repo: String!) {
|
|
89
|
+
repository(owner: $owner, name: $repo) {
|
|
90
|
+
id
|
|
91
|
+
}
|
|
92
|
+
}`;
|
|
93
|
+
const ISSUE_ID_QUERY = `query IssueId($owner: String!, $repo: String!, $number: Int!) {
|
|
94
|
+
repository(owner: $owner, name: $repo) {
|
|
95
|
+
issue(number: $number) {
|
|
96
|
+
id
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}`;
|
|
100
|
+
const ISSUE_PROJECT_ITEM_QUERY = `query IssueProjectItem($owner: String!, $repo: String!, $number: Int!) {
|
|
101
|
+
repository(owner: $owner, name: $repo) {
|
|
102
|
+
issue(number: $number) {
|
|
103
|
+
id
|
|
104
|
+
projectItems(first: 10) {
|
|
105
|
+
nodes {
|
|
106
|
+
id
|
|
107
|
+
project { id }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}`;
|
|
113
|
+
const CREATE_ISSUE_MUTATION = `mutation CreateIssue($input: CreateIssueInput!) {
|
|
114
|
+
createIssue(input: $input) {
|
|
115
|
+
issue {
|
|
116
|
+
id
|
|
117
|
+
number
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}`;
|
|
121
|
+
const ADD_PROJECT_ITEM_MUTATION = `mutation AddProjectItem($input: AddProjectV2ItemByIdInput!) {
|
|
122
|
+
addProjectV2ItemById(input: $input) {
|
|
123
|
+
item {
|
|
124
|
+
id
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}`;
|
|
128
|
+
const UPDATE_STATUS_MUTATION = `mutation UpdateProjectItemStatus($input: UpdateProjectV2ItemFieldValueInput!) {
|
|
129
|
+
updateProjectV2ItemFieldValue(input: $input) {
|
|
130
|
+
projectV2Item {
|
|
131
|
+
id
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}`;
|
|
135
|
+
const UPDATE_ISSUE_MUTATION = `mutation UpdateIssue($input: UpdateIssueInput!) {
|
|
136
|
+
updateIssue(input: $input) {
|
|
137
|
+
issue {
|
|
138
|
+
id
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}`;
|
|
142
|
+
const UPDATE_PROJECT_ITEM_POSITION_MUTATION = `mutation UpdateProjectItemPosition($input: UpdateProjectV2ItemPositionInput!) {
|
|
143
|
+
updateProjectV2ItemPosition(input: $input) {
|
|
144
|
+
clientMutationId
|
|
145
|
+
}
|
|
146
|
+
}`;
|
|
147
|
+
const DELETE_PROJECT_ITEM_MUTATION = `mutation DeleteProjectItem($input: DeleteProjectV2ItemInput!) {
|
|
148
|
+
deleteProjectV2Item(input: $input) {
|
|
149
|
+
deletedItemId
|
|
150
|
+
}
|
|
151
|
+
}`;
|
|
152
|
+
export async function ghIssuesBackend(deps) {
|
|
153
|
+
const client = createGhClient({
|
|
154
|
+
token: deps.token,
|
|
155
|
+
endpoint: deps.endpoint,
|
|
156
|
+
fetch: deps.fetch
|
|
157
|
+
});
|
|
158
|
+
const listName = `${deps.project.owner}/${deps.project.number}`;
|
|
159
|
+
const variables = {
|
|
160
|
+
owner: deps.project.owner,
|
|
161
|
+
number: deps.project.number
|
|
162
|
+
};
|
|
163
|
+
const organizationResult = await client.graphql(PROJECT_ORGANIZATION_QUERY, variables);
|
|
164
|
+
let project = organizationResult.organization?.projectV2 ?? null;
|
|
165
|
+
if (project === null) {
|
|
166
|
+
const userResult = await client.graphql(PROJECT_USER_QUERY, variables);
|
|
167
|
+
project = userResult.user?.projectV2 ?? null;
|
|
168
|
+
}
|
|
169
|
+
if (project === null) {
|
|
170
|
+
throw new Error(`Project ${listName} not found or inaccessible.`);
|
|
171
|
+
}
|
|
172
|
+
const field = project.field;
|
|
173
|
+
if (!isStatusField(field)) {
|
|
174
|
+
throw new Error(`Project ${listName} has no Status field; gh-issues requires one.`);
|
|
175
|
+
}
|
|
176
|
+
if (field.options.length === 0) {
|
|
177
|
+
throw new Error(`Project ${listName} Status field has no options.`);
|
|
178
|
+
}
|
|
179
|
+
const session = createSession(project, field);
|
|
180
|
+
const repoParts = parseRepo(deps.repo);
|
|
181
|
+
const context = {
|
|
182
|
+
client,
|
|
183
|
+
repoOwner: repoParts.owner,
|
|
184
|
+
repoName: repoParts.name,
|
|
185
|
+
issueIds: new Map()
|
|
186
|
+
};
|
|
187
|
+
function list(name) {
|
|
188
|
+
assertSingleList(name, listName);
|
|
189
|
+
return createTasksView(listName, session, context);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
list,
|
|
193
|
+
async lists() {
|
|
194
|
+
return [listName];
|
|
195
|
+
},
|
|
196
|
+
async allTasks(filter) {
|
|
197
|
+
return list(listName).all(filter);
|
|
198
|
+
},
|
|
199
|
+
async get(qualifiedId) {
|
|
200
|
+
const id = parseQualifiedId(qualifiedId, listName);
|
|
201
|
+
return list(listName).get(id);
|
|
202
|
+
},
|
|
203
|
+
async moveBetweenLists(_qualifiedId, _targetList) {
|
|
204
|
+
throw singleListError(listName);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function createSession(project, field) {
|
|
209
|
+
const statusOptions = new Map(field.options.map((option) => [option.name, option.id]));
|
|
210
|
+
const states = field.options.map((option) => option.name);
|
|
211
|
+
const events = Object.fromEntries(states.map((state) => [state, Object.freeze({ from: "*", to: state })]));
|
|
212
|
+
const stateMachine = Object.freeze({
|
|
213
|
+
states: Object.freeze([...states]),
|
|
214
|
+
initial: states[0],
|
|
215
|
+
events: Object.freeze(events)
|
|
216
|
+
});
|
|
217
|
+
return Object.freeze({
|
|
218
|
+
projectId: project.id,
|
|
219
|
+
statusFieldId: field.id,
|
|
220
|
+
statusOptions,
|
|
221
|
+
stateMachine
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function createTasksView(name, session, context) {
|
|
225
|
+
return {
|
|
226
|
+
name,
|
|
227
|
+
stateMachine: session.stateMachine,
|
|
228
|
+
async all(filter) {
|
|
229
|
+
if (filter?.includeArchived === true) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
const tasks = await fetchProjectTasks(name, session, context);
|
|
233
|
+
const filteredTasks = filter?.state === undefined ? tasks : tasks.filter((task) => task.state === filter.state);
|
|
234
|
+
if (filter?.order === "alphabetical") {
|
|
235
|
+
return sortTasks(filteredTasks);
|
|
236
|
+
}
|
|
237
|
+
if (filter?.order === "created") {
|
|
238
|
+
return applyOrder(filteredTasks.map((task) => ({
|
|
239
|
+
task,
|
|
240
|
+
raw: {
|
|
241
|
+
created: task.metadata.created
|
|
242
|
+
}
|
|
243
|
+
})), "created");
|
|
244
|
+
}
|
|
245
|
+
return filteredTasks;
|
|
246
|
+
},
|
|
247
|
+
async get(id) {
|
|
248
|
+
return fetchIssueTask(id, name, session, context);
|
|
249
|
+
},
|
|
250
|
+
/**
|
|
251
|
+
* GitHub Issues assigns the issue number, so TaskCreate.id is intentionally ignored.
|
|
252
|
+
*/
|
|
253
|
+
async create(input) {
|
|
254
|
+
const repositoryId = await resolveRepositoryId(context);
|
|
255
|
+
const created = await context.client.graphql(CREATE_ISSUE_MUTATION, {
|
|
256
|
+
input: {
|
|
257
|
+
repositoryId,
|
|
258
|
+
title: input.name,
|
|
259
|
+
body: input.description ?? ""
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
const issue = created.createIssue?.issue;
|
|
263
|
+
const issueId = issue?.id ?? null;
|
|
264
|
+
const issueNumber = issue?.number ?? null;
|
|
265
|
+
if (issueId === null || issueNumber === null) {
|
|
266
|
+
throw new Error("GitHub createIssue response did not include issue id and number.");
|
|
267
|
+
}
|
|
268
|
+
context.issueIds.set(issueNumber, issueId);
|
|
269
|
+
const added = await context.client.graphql(ADD_PROJECT_ITEM_MUTATION, {
|
|
270
|
+
input: {
|
|
271
|
+
projectId: session.projectId,
|
|
272
|
+
contentId: issueId
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
const projectItemId = added.addProjectV2ItemById?.item?.id ?? null;
|
|
276
|
+
if (projectItemId === null) {
|
|
277
|
+
throw new Error("GitHub addProjectV2ItemById response did not include project item id.");
|
|
278
|
+
}
|
|
279
|
+
await updateProjectItemStatus(projectItemId, session.stateMachine.initial, session, context);
|
|
280
|
+
return fetchIssueTask(String(issueNumber), name, session, context);
|
|
281
|
+
},
|
|
282
|
+
async update(id, patch) {
|
|
283
|
+
const input = {};
|
|
284
|
+
if (patch.name !== undefined) {
|
|
285
|
+
input.title = patch.name;
|
|
286
|
+
}
|
|
287
|
+
if (patch.description !== undefined) {
|
|
288
|
+
input.body = patch.description;
|
|
289
|
+
}
|
|
290
|
+
// metadata writes are out of scope for v1 on gh-issues.
|
|
291
|
+
if (Object.keys(input).length > 0) {
|
|
292
|
+
input.id = await resolveIssueId(id, name, context);
|
|
293
|
+
await context.client.graphql(UPDATE_ISSUE_MUTATION, {
|
|
294
|
+
input
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return fetchIssueTask(id, name, session, context);
|
|
298
|
+
},
|
|
299
|
+
async fire(id, event, _opts) {
|
|
300
|
+
if (!session.statusOptions.has(event)) {
|
|
301
|
+
throw new InvalidTransitionError({
|
|
302
|
+
event,
|
|
303
|
+
to: event,
|
|
304
|
+
reason: `Unknown gh-issues Status state "${event}".`
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const projectItemId = await resolveProjectItemId(id, name, session, context);
|
|
308
|
+
// opts.metadataPatch writes are out of scope for v1 on gh-issues.
|
|
309
|
+
await updateProjectItemStatus(projectItemId, event, session, context);
|
|
310
|
+
return fetchIssueTask(id, name, session, context);
|
|
311
|
+
},
|
|
312
|
+
async canFire(id, event) {
|
|
313
|
+
return findEvent(session.stateMachine, id, event) !== undefined;
|
|
314
|
+
},
|
|
315
|
+
async events(id) {
|
|
316
|
+
return eventsFromState(session.stateMachine, id);
|
|
317
|
+
},
|
|
318
|
+
async delete(id) {
|
|
319
|
+
const projectItemId = await resolveProjectItemId(id, name, session, context);
|
|
320
|
+
await context.client.graphql(DELETE_PROJECT_ITEM_MUTATION, {
|
|
321
|
+
input: {
|
|
322
|
+
projectId: session.projectId,
|
|
323
|
+
itemId: projectItemId
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
async move(id, anchor) {
|
|
328
|
+
const projectItemId = await resolveProjectItemId(id, name, session, context);
|
|
329
|
+
const afterId = await resolveMoveAfterId(id, anchor, name, session, context);
|
|
330
|
+
await updateProjectItemPosition(projectItemId, afterId, session, context);
|
|
331
|
+
return fetchIssueTask(id, name, session, context);
|
|
332
|
+
},
|
|
333
|
+
async reorder(ids) {
|
|
334
|
+
const currentTasks = await fetchProjectTasks(name, session, context);
|
|
335
|
+
const currentIds = currentTasks.map((task) => task.id);
|
|
336
|
+
const currentSet = new Set(currentIds);
|
|
337
|
+
const inputSet = new Set(ids);
|
|
338
|
+
const seenInputIds = new Set();
|
|
339
|
+
const missing = currentIds.filter((id) => !inputSet.has(id));
|
|
340
|
+
const extra = ids.filter((id) => {
|
|
341
|
+
if (!currentSet.has(id)) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
if (seenInputIds.has(id)) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
seenInputIds.add(id);
|
|
348
|
+
return false;
|
|
349
|
+
});
|
|
350
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
351
|
+
throw new OrderMismatchError({ missing, extra });
|
|
352
|
+
}
|
|
353
|
+
const itemIdsByTaskId = new Map(currentTasks.map((task) => [task.id, projectItemIdFromTask(task)]));
|
|
354
|
+
let afterId = null;
|
|
355
|
+
for (const id of ids) {
|
|
356
|
+
const projectItemId = itemIdsByTaskId.get(id);
|
|
357
|
+
if (projectItemId === undefined) {
|
|
358
|
+
throw new OrderMismatchError({ missing: [id], extra: [] });
|
|
359
|
+
}
|
|
360
|
+
await updateProjectItemPosition(projectItemId, afterId, session, context);
|
|
361
|
+
afterId = projectItemId;
|
|
362
|
+
}
|
|
363
|
+
return fetchProjectTasks(name, session, context);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async function resolveRepositoryId(context) {
|
|
368
|
+
if (context.repositoryId !== undefined) {
|
|
369
|
+
return context.repositoryId;
|
|
370
|
+
}
|
|
371
|
+
const result = await context.client.graphql(REPOSITORY_QUERY, {
|
|
372
|
+
owner: context.repoOwner,
|
|
373
|
+
repo: context.repoName
|
|
374
|
+
});
|
|
375
|
+
const repositoryId = result.repository?.id ?? null;
|
|
376
|
+
if (repositoryId === null) {
|
|
377
|
+
throw new Error(`Repository ${context.repoOwner}/${context.repoName} not found or inaccessible.`);
|
|
378
|
+
}
|
|
379
|
+
context.repositoryId = repositoryId;
|
|
380
|
+
return repositoryId;
|
|
381
|
+
}
|
|
382
|
+
async function resolveIssueId(id, listName, context) {
|
|
383
|
+
const issueNumber = parseIssueNumber(id, listName);
|
|
384
|
+
const cachedIssueId = context.issueIds.get(issueNumber);
|
|
385
|
+
if (cachedIssueId !== undefined) {
|
|
386
|
+
return cachedIssueId;
|
|
387
|
+
}
|
|
388
|
+
const result = await context.client.graphql(ISSUE_ID_QUERY, {
|
|
389
|
+
owner: context.repoOwner,
|
|
390
|
+
repo: context.repoName,
|
|
391
|
+
number: issueNumber
|
|
392
|
+
});
|
|
393
|
+
const issueId = result.repository?.issue?.id ?? null;
|
|
394
|
+
if (issueId === null) {
|
|
395
|
+
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
396
|
+
}
|
|
397
|
+
context.issueIds.set(issueNumber, issueId);
|
|
398
|
+
return issueId;
|
|
399
|
+
}
|
|
400
|
+
async function resolveProjectItemId(id, listName, session, context) {
|
|
401
|
+
const issueNumber = parseIssueNumber(id, listName);
|
|
402
|
+
const result = await context.client.graphql(ISSUE_PROJECT_ITEM_QUERY, {
|
|
403
|
+
owner: context.repoOwner,
|
|
404
|
+
repo: context.repoName,
|
|
405
|
+
number: issueNumber
|
|
406
|
+
});
|
|
407
|
+
const issue = result.repository?.issue ?? null;
|
|
408
|
+
if (issue === null) {
|
|
409
|
+
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
410
|
+
}
|
|
411
|
+
if (issue.id !== undefined && issue.id !== null) {
|
|
412
|
+
context.issueIds.set(issueNumber, issue.id);
|
|
413
|
+
}
|
|
414
|
+
const projectItem = issue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ?? null;
|
|
415
|
+
if (projectItem === null) {
|
|
416
|
+
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
417
|
+
}
|
|
418
|
+
return projectItem.id;
|
|
419
|
+
}
|
|
420
|
+
async function updateProjectItemStatus(projectItemId, state, session, context) {
|
|
421
|
+
const optionId = session.statusOptions.get(state);
|
|
422
|
+
if (optionId === undefined) {
|
|
423
|
+
throw new InvalidTransitionError({
|
|
424
|
+
to: state,
|
|
425
|
+
reason: `Unknown gh-issues Status state "${state}".`
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
await context.client.graphql(UPDATE_STATUS_MUTATION, {
|
|
429
|
+
input: {
|
|
430
|
+
projectId: session.projectId,
|
|
431
|
+
itemId: projectItemId,
|
|
432
|
+
fieldId: session.statusFieldId,
|
|
433
|
+
value: {
|
|
434
|
+
singleSelectOptionId: optionId
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
async function updateProjectItemPosition(projectItemId, afterId, session, context) {
|
|
440
|
+
await context.client.graphql(UPDATE_PROJECT_ITEM_POSITION_MUTATION, {
|
|
441
|
+
input: {
|
|
442
|
+
projectId: session.projectId,
|
|
443
|
+
itemId: projectItemId,
|
|
444
|
+
afterId
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
async function resolveMoveAfterId(movingId, anchor, listName, session, context) {
|
|
449
|
+
if ("position" in anchor) {
|
|
450
|
+
if (anchor.position === "top") {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
const tasks = await fetchProjectTasks(listName, session, context);
|
|
454
|
+
for (let index = tasks.length - 1; index >= 0; index -= 1) {
|
|
455
|
+
const task = tasks[index];
|
|
456
|
+
if (task.id !== movingId) {
|
|
457
|
+
return projectItemIdFromTask(task);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const anchorId = "before" in anchor ? anchor.before : anchor.after;
|
|
463
|
+
let anchorProjectItemId;
|
|
464
|
+
try {
|
|
465
|
+
anchorProjectItemId = await resolveProjectItemId(anchorId, listName, session, context);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (error instanceof TaskNotFoundError) {
|
|
469
|
+
throw new AnchorNotFoundError(anchorId);
|
|
470
|
+
}
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
if ("after" in anchor) {
|
|
474
|
+
return anchorProjectItemId;
|
|
475
|
+
}
|
|
476
|
+
const tasks = await fetchProjectTasks(listName, session, context);
|
|
477
|
+
const anchorIndex = tasks.findIndex((task) => task.id === anchorId);
|
|
478
|
+
if (anchorIndex < 0) {
|
|
479
|
+
throw new AnchorNotFoundError(anchorId);
|
|
480
|
+
}
|
|
481
|
+
for (let index = anchorIndex - 1; index >= 0; index -= 1) {
|
|
482
|
+
const predecessor = tasks[index];
|
|
483
|
+
if (predecessor.id !== movingId) {
|
|
484
|
+
return projectItemIdFromTask(predecessor);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
async function fetchProjectTasks(listName, session, context) {
|
|
490
|
+
const tasks = [];
|
|
491
|
+
let after = null;
|
|
492
|
+
do {
|
|
493
|
+
const result = await context.client.graphql(PROJECT_ITEMS_QUERY, {
|
|
494
|
+
projectId: session.projectId,
|
|
495
|
+
after
|
|
496
|
+
});
|
|
497
|
+
const items = result.node?.items;
|
|
498
|
+
for (const item of items?.nodes ?? []) {
|
|
499
|
+
const task = mapProjectItemToTask(item, listName, session);
|
|
500
|
+
if (task !== null) {
|
|
501
|
+
tasks.push(task);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
after = items?.pageInfo?.hasNextPage === true ? (items.pageInfo.endCursor ?? null) : null;
|
|
505
|
+
} while (after !== null);
|
|
506
|
+
return tasks;
|
|
507
|
+
}
|
|
508
|
+
async function fetchIssueTask(id, listName, session, context) {
|
|
509
|
+
const issueNumber = parseIssueNumber(id, listName);
|
|
510
|
+
const result = await context.client.graphql(ISSUE_QUERY, {
|
|
511
|
+
owner: context.repoOwner,
|
|
512
|
+
repo: context.repoName,
|
|
513
|
+
number: issueNumber
|
|
514
|
+
});
|
|
515
|
+
const issue = result.repository?.issue ?? null;
|
|
516
|
+
if (issue === null) {
|
|
517
|
+
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
518
|
+
}
|
|
519
|
+
const projectItem = issue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ?? null;
|
|
520
|
+
if (projectItem === null) {
|
|
521
|
+
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
522
|
+
}
|
|
523
|
+
return mapIssueToTask({
|
|
524
|
+
issue,
|
|
525
|
+
projectItemId: projectItem.id,
|
|
526
|
+
statusName: projectItem.fieldValueByName?.name ?? null,
|
|
527
|
+
listName,
|
|
528
|
+
initialState: session.stateMachine.initial
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function parseIssueNumber(id, listName) {
|
|
532
|
+
const issueNumber = Number(id);
|
|
533
|
+
if (!Number.isInteger(issueNumber) || issueNumber < 1) {
|
|
534
|
+
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
535
|
+
}
|
|
536
|
+
return issueNumber;
|
|
537
|
+
}
|
|
538
|
+
function mapProjectItemToTask(item, listName, session) {
|
|
539
|
+
const content = item.content;
|
|
540
|
+
if (!isIssueNode(content)) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
return mapIssueToTask({
|
|
544
|
+
issue: content,
|
|
545
|
+
projectItemId: item.id,
|
|
546
|
+
statusName: item.fieldValueByName?.name ?? null,
|
|
547
|
+
listName,
|
|
548
|
+
initialState: session.stateMachine.initial
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function isIssueNode(value) {
|
|
552
|
+
return (isRecord(value) &&
|
|
553
|
+
value.__typename === "Issue" &&
|
|
554
|
+
typeof value.number === "number" &&
|
|
555
|
+
typeof value.title === "string" &&
|
|
556
|
+
typeof value.url === "string" &&
|
|
557
|
+
typeof value.createdAt === "string");
|
|
558
|
+
}
|
|
559
|
+
function mapIssueToTask(options) {
|
|
560
|
+
const id = String(options.issue.number);
|
|
561
|
+
const labels = (options.issue.labels?.nodes ?? [])
|
|
562
|
+
.filter((node) => node !== null)
|
|
563
|
+
.map((node) => node.name);
|
|
564
|
+
const assignees = (options.issue.assignees?.nodes ?? [])
|
|
565
|
+
.filter((node) => node !== null)
|
|
566
|
+
.map((node) => node.login);
|
|
567
|
+
return {
|
|
568
|
+
list: options.listName,
|
|
569
|
+
id,
|
|
570
|
+
qualifiedId: `${options.listName}/${id}`,
|
|
571
|
+
name: options.issue.title,
|
|
572
|
+
description: options.issue.body ?? "",
|
|
573
|
+
state: options.statusName ?? options.initialState,
|
|
574
|
+
metadata: {
|
|
575
|
+
url: options.issue.url,
|
|
576
|
+
labels,
|
|
577
|
+
assignees,
|
|
578
|
+
milestone: options.issue.milestone?.title ?? null,
|
|
579
|
+
projectItemId: options.projectItemId,
|
|
580
|
+
created: options.issue.createdAt
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function projectItemIdFromTask(task) {
|
|
585
|
+
const projectItemId = task.metadata.projectItemId;
|
|
586
|
+
if (typeof projectItemId !== "string") {
|
|
587
|
+
throw new Error(`Task "${task.qualifiedId}" is missing GitHub project item metadata.`);
|
|
588
|
+
}
|
|
589
|
+
return projectItemId;
|
|
590
|
+
}
|
|
591
|
+
function parseRepo(repo) {
|
|
592
|
+
const parts = repo.split("/");
|
|
593
|
+
if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
|
|
594
|
+
throw new Error(`Invalid GitHub repository "${repo}". Expected "owner/name".`);
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
owner: parts[0],
|
|
598
|
+
name: parts[1]
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function isRecord(value) {
|
|
602
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
603
|
+
}
|
|
604
|
+
function isStatusOption(value) {
|
|
605
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.name === "string";
|
|
606
|
+
}
|
|
607
|
+
function isStatusField(value) {
|
|
608
|
+
return (isRecord(value) &&
|
|
609
|
+
typeof value.id === "string" &&
|
|
610
|
+
Array.isArray(value.options) &&
|
|
611
|
+
value.options.every(isStatusOption));
|
|
612
|
+
}
|
|
613
|
+
function assertSingleList(name, listName) {
|
|
614
|
+
if (name !== listName) {
|
|
615
|
+
throw singleListError(listName);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function singleListError(listName) {
|
|
619
|
+
return new Error(`gh-issues backend has a single list ${listName}`);
|
|
620
|
+
}
|
|
621
|
+
function parseQualifiedId(qualifiedId, listName) {
|
|
622
|
+
const prefix = `${listName}/`;
|
|
623
|
+
if (!qualifiedId.startsWith(prefix) || qualifiedId.length === prefix.length) {
|
|
624
|
+
throw new Error(`Invalid qualified task id "${qualifiedId}".`);
|
|
625
|
+
}
|
|
626
|
+
return qualifiedId.slice(prefix.length);
|
|
627
|
+
}
|