@evref-bl/dev-nexus 0.1.0-alpha.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/README.md +677 -0
- package/dist/browserOpener.d.ts +9 -0
- package/dist/browserOpener.js +47 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +2374 -0
- package/dist/gitWorktreeService.d.ts +57 -0
- package/dist/gitWorktreeService.js +157 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +47 -0
- package/dist/nexusAgentMcpConfig.d.ts +30 -0
- package/dist/nexusAgentMcpConfig.js +228 -0
- package/dist/nexusAutomation.d.ts +103 -0
- package/dist/nexusAutomation.js +390 -0
- package/dist/nexusAutomationAgentLaunch.d.ts +148 -0
- package/dist/nexusAutomationAgentLaunch.js +855 -0
- package/dist/nexusAutomationAgentProfile.d.ts +39 -0
- package/dist/nexusAutomationAgentProfile.js +103 -0
- package/dist/nexusAutomationAgentSurface.d.ts +62 -0
- package/dist/nexusAutomationAgentSurface.js +90 -0
- package/dist/nexusAutomationCommandExecutor.d.ts +29 -0
- package/dist/nexusAutomationCommandExecutor.js +251 -0
- package/dist/nexusAutomationConfig.d.ts +114 -0
- package/dist/nexusAutomationConfig.js +547 -0
- package/dist/nexusAutomationEnqueue.d.ts +37 -0
- package/dist/nexusAutomationEnqueue.js +128 -0
- package/dist/nexusAutomationRunOnce.d.ts +91 -0
- package/dist/nexusAutomationRunOnce.js +586 -0
- package/dist/nexusAutomationScheduler.d.ts +50 -0
- package/dist/nexusAutomationScheduler.js +196 -0
- package/dist/nexusAutomationStatus.d.ts +55 -0
- package/dist/nexusAutomationStatus.js +462 -0
- package/dist/nexusAutomationTarget.d.ts +19 -0
- package/dist/nexusAutomationTarget.js +33 -0
- package/dist/nexusAutomationTargetCycle.d.ts +90 -0
- package/dist/nexusAutomationTargetCycle.js +282 -0
- package/dist/nexusAutomationTargetReport.d.ts +136 -0
- package/dist/nexusAutomationTargetReport.js +504 -0
- package/dist/nexusAutomationWorktreeSetup.d.ts +89 -0
- package/dist/nexusAutomationWorktreeSetup.js +661 -0
- package/dist/nexusCoordination.d.ts +198 -0
- package/dist/nexusCoordination.js +1018 -0
- package/dist/nexusExtension.d.ts +31 -0
- package/dist/nexusExtension.js +1 -0
- package/dist/nexusHomeConfig.d.ts +38 -0
- package/dist/nexusHomeConfig.js +133 -0
- package/dist/nexusMcpServer.d.ts +31 -0
- package/dist/nexusMcpServer.js +1036 -0
- package/dist/nexusPluginCapabilities.d.ts +197 -0
- package/dist/nexusPluginCapabilities.js +201 -0
- package/dist/nexusProjectConfig.d.ts +95 -0
- package/dist/nexusProjectConfig.js +880 -0
- package/dist/nexusProjectHomeService.d.ts +121 -0
- package/dist/nexusProjectHomeService.js +171 -0
- package/dist/nexusProjectLifecycle.d.ts +62 -0
- package/dist/nexusProjectLifecycle.js +205 -0
- package/dist/nexusProjectOperations.d.ts +101 -0
- package/dist/nexusProjectOperations.js +296 -0
- package/dist/nexusProjectRegistry.d.ts +42 -0
- package/dist/nexusProjectRegistry.js +91 -0
- package/dist/nexusProjectScaffold.d.ts +25 -0
- package/dist/nexusProjectScaffold.js +61 -0
- package/dist/nexusProjectTemplate.d.ts +34 -0
- package/dist/nexusProjectTemplate.js +354 -0
- package/dist/nexusSkills.d.ts +134 -0
- package/dist/nexusSkills.js +647 -0
- package/dist/nexusWorkerContextBundle.d.ts +142 -0
- package/dist/nexusWorkerContextBundle.js +375 -0
- package/dist/processSupervisor.d.ts +89 -0
- package/dist/processSupervisor.js +440 -0
- package/dist/vibeKanbanApi.d.ts +11 -0
- package/dist/vibeKanbanApi.js +14 -0
- package/dist/vibeKanbanAuth.d.ts +25 -0
- package/dist/vibeKanbanAuth.js +101 -0
- package/dist/vibeKanbanBoardAdapter.d.ts +36 -0
- package/dist/vibeKanbanBoardAdapter.js +196 -0
- package/dist/vibeKanbanMcpConfig.d.ts +36 -0
- package/dist/vibeKanbanMcpConfig.js +191 -0
- package/dist/vibeKanbanProjectAdapter.d.ts +39 -0
- package/dist/vibeKanbanProjectAdapter.js +113 -0
- package/dist/vibeKanbanWorkspaceSetup.d.ts +1 -0
- package/dist/vibeKanbanWorkspaceSetup.js +96 -0
- package/dist/workItemService.d.ts +60 -0
- package/dist/workItemService.js +163 -0
- package/dist/workTrackingGitHubProvider.d.ts +71 -0
- package/dist/workTrackingGitHubProvider.js +663 -0
- package/dist/workTrackingGitLabProvider.d.ts +62 -0
- package/dist/workTrackingGitLabProvider.js +523 -0
- package/dist/workTrackingJiraProvider.d.ts +67 -0
- package/dist/workTrackingJiraProvider.js +652 -0
- package/dist/workTrackingLocalProvider.d.ts +49 -0
- package/dist/workTrackingLocalProvider.js +463 -0
- package/dist/workTrackingProviderService.d.ts +21 -0
- package/dist/workTrackingProviderService.js +117 -0
- package/dist/workTrackingTypes.d.ts +202 -0
- package/dist/workTrackingTypes.js +1 -0
- package/dist/workTrackingVibeProvider.d.ts +35 -0
- package/dist/workTrackingVibeProvider.js +119 -0
- package/dist/worktreeExecutionMetadata.d.ts +76 -0
- package/dist/worktreeExecutionMetadata.js +239 -0
- package/package.json +37 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
export const defaultGitHubApiBaseUrl = "https://api.github.com";
|
|
3
|
+
export const defaultGitHubApiVersion = "2026-03-10";
|
|
4
|
+
export const githubStatusLabelPrefix = "status:";
|
|
5
|
+
const openStatuses = new Set([
|
|
6
|
+
"todo",
|
|
7
|
+
"ready",
|
|
8
|
+
"in_progress",
|
|
9
|
+
"blocked",
|
|
10
|
+
]);
|
|
11
|
+
const closedStatuses = new Set(["done", "wont_do"]);
|
|
12
|
+
const workStatuses = new Set([
|
|
13
|
+
...openStatuses,
|
|
14
|
+
...closedStatuses,
|
|
15
|
+
]);
|
|
16
|
+
export class GitHubWorkTrackerProviderError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "GitHubWorkTrackerProviderError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const githubWorkTrackerCapabilities = {
|
|
23
|
+
createItem: true,
|
|
24
|
+
listItems: true,
|
|
25
|
+
getItem: true,
|
|
26
|
+
updateItem: true,
|
|
27
|
+
comment: true,
|
|
28
|
+
labels: true,
|
|
29
|
+
assignees: true,
|
|
30
|
+
milestones: true,
|
|
31
|
+
board: false,
|
|
32
|
+
boardStatus: false,
|
|
33
|
+
draftItems: false,
|
|
34
|
+
webhooks: false,
|
|
35
|
+
};
|
|
36
|
+
export function githubWorkTrackerCapabilitiesForConfig(config) {
|
|
37
|
+
const board = githubProjectV2BoardConfig(config);
|
|
38
|
+
const statusOptions = board?.statusOptions ?? {};
|
|
39
|
+
return {
|
|
40
|
+
...githubWorkTrackerCapabilities,
|
|
41
|
+
board: Boolean(board),
|
|
42
|
+
boardStatus: Boolean(board?.statusFieldId && Object.keys(statusOptions).length > 0),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function createGitHubWorkTrackerProvider(options) {
|
|
46
|
+
return new GitHubWorkTrackerProvider(options);
|
|
47
|
+
}
|
|
48
|
+
export class GitHubWorkTrackerProvider {
|
|
49
|
+
provider = "github";
|
|
50
|
+
capabilities;
|
|
51
|
+
config;
|
|
52
|
+
fetchFn;
|
|
53
|
+
apiBaseUrl;
|
|
54
|
+
graphqlApiUrl;
|
|
55
|
+
apiVersion;
|
|
56
|
+
staticAuthorizationHeader;
|
|
57
|
+
credentialRunner;
|
|
58
|
+
credentialInteractive;
|
|
59
|
+
credentialAuthorizationHeader;
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.config = options.config;
|
|
62
|
+
this.fetchFn = options.fetch ?? fetch;
|
|
63
|
+
this.apiBaseUrl = normalizeGitHubApiBaseUrl(options.apiBaseUrl ?? options.config.host);
|
|
64
|
+
this.graphqlApiUrl = normalizeGitHubGraphQLApiUrl(options.graphqlApiUrl ?? options.config.host);
|
|
65
|
+
this.apiVersion = requiredNonEmptyString(options.apiVersion ?? defaultGitHubApiVersion, "apiVersion");
|
|
66
|
+
this.capabilities = githubWorkTrackerCapabilitiesForConfig(options.config);
|
|
67
|
+
const token = optionalNonEmptyString(options.token, "token") ??
|
|
68
|
+
optionalNonEmptyString((options.env ?? process.env).GITHUB_TOKEN, "GITHUB_TOKEN") ??
|
|
69
|
+
optionalNonEmptyString((options.env ?? process.env).GH_TOKEN, "GH_TOKEN");
|
|
70
|
+
this.staticAuthorizationHeader = token ? `Bearer ${token}` : undefined;
|
|
71
|
+
this.credentialRunner =
|
|
72
|
+
options.credentialRunner === false
|
|
73
|
+
? undefined
|
|
74
|
+
: options.credentialRunner ?? defaultGitCredentialRunner;
|
|
75
|
+
this.credentialInteractive = options.credentialInteractive ?? false;
|
|
76
|
+
}
|
|
77
|
+
async createWorkItem(input) {
|
|
78
|
+
const status = input.status ?? "todo";
|
|
79
|
+
assertWorkStatus(status);
|
|
80
|
+
const created = await this.requestJson("POST", this.issuePath(), {
|
|
81
|
+
title: requiredNonEmptyString(input.title, "title"),
|
|
82
|
+
...(input.description !== undefined ? { body: input.description } : {}),
|
|
83
|
+
...requestArray("labels", labelsWithStatus(input.labels, status)),
|
|
84
|
+
...requestArray("assignees", normalizeStringArray(input.assignees, "assignees")),
|
|
85
|
+
...requestMilestone(input.milestone),
|
|
86
|
+
});
|
|
87
|
+
if (closedStatuses.has(status)) {
|
|
88
|
+
return this.updateWorkItem({ id: String(created.number) }, { status });
|
|
89
|
+
}
|
|
90
|
+
await this.addIssueToConfiguredProject(created, status);
|
|
91
|
+
return this.issueToWorkItem(created);
|
|
92
|
+
}
|
|
93
|
+
async listWorkItems(query = {}) {
|
|
94
|
+
const statuses = normalizeStatusFilter(query.status);
|
|
95
|
+
const state = githubStateForQuery(statuses);
|
|
96
|
+
const limit = normalizeLimit(query.limit);
|
|
97
|
+
const params = new URLSearchParams({
|
|
98
|
+
state,
|
|
99
|
+
per_page: String(limit ? Math.min(Math.max(limit, 1), 100) : 100),
|
|
100
|
+
page: "1",
|
|
101
|
+
});
|
|
102
|
+
const labels = normalizeStringArray(query.labels, "labels");
|
|
103
|
+
if (labels.length > 0) {
|
|
104
|
+
params.set("labels", labels.join(","));
|
|
105
|
+
}
|
|
106
|
+
const assignees = normalizeStringArray(query.assignees, "assignees");
|
|
107
|
+
if (assignees.length === 1) {
|
|
108
|
+
params.set("assignee", assignees[0]);
|
|
109
|
+
}
|
|
110
|
+
const issues = await this.requestJson("GET", `${this.issuePath()}?${params.toString()}`);
|
|
111
|
+
const search = query.search?.trim().toLowerCase();
|
|
112
|
+
const items = issues
|
|
113
|
+
.filter((issue) => !issue.pull_request)
|
|
114
|
+
.map((issue) => this.issueToWorkItem(issue))
|
|
115
|
+
.filter((item) => matchesStatusFilter(item, statuses))
|
|
116
|
+
.filter((item) => matchesStringFilter(item.assignees, assignees))
|
|
117
|
+
.filter((item) => !search || matchesSearch(item, search));
|
|
118
|
+
return limit === undefined ? items : items.slice(0, limit);
|
|
119
|
+
}
|
|
120
|
+
async getWorkItem(ref) {
|
|
121
|
+
return this.issueToWorkItem(await this.getIssue(ref));
|
|
122
|
+
}
|
|
123
|
+
async updateWorkItem(ref, patch) {
|
|
124
|
+
const issueNumber = issueNumberFromRef(ref);
|
|
125
|
+
const body = {};
|
|
126
|
+
if (patch.title !== undefined) {
|
|
127
|
+
body.title = requiredNonEmptyString(patch.title, "title");
|
|
128
|
+
}
|
|
129
|
+
if (patch.description !== undefined) {
|
|
130
|
+
body.body = patch.description;
|
|
131
|
+
}
|
|
132
|
+
if (patch.assignees !== undefined) {
|
|
133
|
+
body.assignees = normalizeStringArray(patch.assignees, "assignees");
|
|
134
|
+
}
|
|
135
|
+
if (patch.milestone !== undefined) {
|
|
136
|
+
Object.assign(body, requestMilestone(patch.milestone));
|
|
137
|
+
}
|
|
138
|
+
if (patch.status !== undefined) {
|
|
139
|
+
assertWorkStatus(patch.status);
|
|
140
|
+
body.state = githubStateForStatus(patch.status);
|
|
141
|
+
if (closedStatuses.has(patch.status)) {
|
|
142
|
+
body.state_reason =
|
|
143
|
+
patch.status === "wont_do" ? "not_planned" : "completed";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (patch.labels !== undefined || patch.status !== undefined) {
|
|
147
|
+
const baseLabels = patch.labels !== undefined
|
|
148
|
+
? normalizeStringArray(patch.labels, "labels")
|
|
149
|
+
: labelNames(await this.getIssue(ref));
|
|
150
|
+
body.labels = labelsWithStatus(baseLabels, patch.status);
|
|
151
|
+
}
|
|
152
|
+
const updated = await this.requestJson("PATCH", `${this.issuePath()}/${issueNumber}`, body);
|
|
153
|
+
if (patch.status !== undefined) {
|
|
154
|
+
await this.addIssueToConfiguredProject(updated, patch.status);
|
|
155
|
+
}
|
|
156
|
+
return this.issueToWorkItem(updated);
|
|
157
|
+
}
|
|
158
|
+
async addComment(ref, body) {
|
|
159
|
+
const issueNumber = issueNumberFromRef(ref);
|
|
160
|
+
const comment = await this.requestJson("POST", `${this.issuePath()}/${issueNumber}/comments`, {
|
|
161
|
+
body: requiredNonEmptyString(body, "body"),
|
|
162
|
+
});
|
|
163
|
+
return this.commentToWorkComment(comment, issueNumber);
|
|
164
|
+
}
|
|
165
|
+
async setStatus(ref, status) {
|
|
166
|
+
return this.updateWorkItem(ref, { status });
|
|
167
|
+
}
|
|
168
|
+
async getIssue(ref) {
|
|
169
|
+
const issueNumber = issueNumberFromRef(ref);
|
|
170
|
+
return this.requestJson("GET", `${this.issuePath()}/${issueNumber}`);
|
|
171
|
+
}
|
|
172
|
+
async addIssueToConfiguredProject(issue, status) {
|
|
173
|
+
const board = githubProjectV2BoardConfig(this.config);
|
|
174
|
+
if (!board) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const contentId = requiredNonEmptyString(issue.node_id, "issue.node_id");
|
|
178
|
+
const addResult = await this.graphql(`mutation AddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
179
|
+
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
|
|
180
|
+
item {
|
|
181
|
+
id
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}`, {
|
|
185
|
+
projectId: board.projectId,
|
|
186
|
+
contentId,
|
|
187
|
+
});
|
|
188
|
+
const itemId = requiredNonEmptyString(addResult.addProjectV2ItemById.item.id, "addProjectV2ItemById.item.id");
|
|
189
|
+
const statusOptionId = status ? board.statusOptions?.[status] : undefined;
|
|
190
|
+
if (board.statusFieldId && statusOptionId) {
|
|
191
|
+
await this.graphql(`mutation UpdateProjectItemStatus(
|
|
192
|
+
$projectId: ID!
|
|
193
|
+
$itemId: ID!
|
|
194
|
+
$fieldId: ID!
|
|
195
|
+
$optionId: String!
|
|
196
|
+
) {
|
|
197
|
+
updateProjectV2ItemFieldValue(
|
|
198
|
+
input: {
|
|
199
|
+
projectId: $projectId
|
|
200
|
+
itemId: $itemId
|
|
201
|
+
fieldId: $fieldId
|
|
202
|
+
value: { singleSelectOptionId: $optionId }
|
|
203
|
+
}
|
|
204
|
+
) {
|
|
205
|
+
projectV2Item {
|
|
206
|
+
id
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}`, {
|
|
210
|
+
projectId: board.projectId,
|
|
211
|
+
itemId,
|
|
212
|
+
fieldId: board.statusFieldId,
|
|
213
|
+
optionId: statusOptionId,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return itemId;
|
|
217
|
+
}
|
|
218
|
+
issuePath() {
|
|
219
|
+
return `/repos/${encodePathSegment(this.config.repository.owner)}/${encodePathSegment(this.config.repository.name)}/issues`;
|
|
220
|
+
}
|
|
221
|
+
async requestJson(method, pathAndQuery, body) {
|
|
222
|
+
const url = new URL(pathAndQuery.replace(/^\/+/, ""), `${this.apiBaseUrl}/`);
|
|
223
|
+
const headers = {
|
|
224
|
+
Accept: "application/vnd.github+json",
|
|
225
|
+
"User-Agent": "dev-nexus",
|
|
226
|
+
"X-GitHub-Api-Version": this.apiVersion,
|
|
227
|
+
};
|
|
228
|
+
if (body !== undefined) {
|
|
229
|
+
headers["Content-Type"] = "application/json";
|
|
230
|
+
}
|
|
231
|
+
const authorizationHeader = this.authorizationHeader();
|
|
232
|
+
if (authorizationHeader) {
|
|
233
|
+
headers.Authorization = authorizationHeader;
|
|
234
|
+
}
|
|
235
|
+
const response = await this.fetchFn(url, {
|
|
236
|
+
method,
|
|
237
|
+
headers,
|
|
238
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
const message = await githubErrorMessage(response, method, url);
|
|
242
|
+
throw new GitHubWorkTrackerProviderError(gitHubErrorMessageWithCredentialHint(message, response, authorizationHeader, this.config));
|
|
243
|
+
}
|
|
244
|
+
return (await response.json());
|
|
245
|
+
}
|
|
246
|
+
async graphql(query, variables) {
|
|
247
|
+
const headers = {
|
|
248
|
+
Accept: "application/vnd.github+json",
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
"User-Agent": "dev-nexus",
|
|
251
|
+
"X-GitHub-Api-Version": this.apiVersion,
|
|
252
|
+
};
|
|
253
|
+
const authorizationHeader = this.authorizationHeader();
|
|
254
|
+
if (authorizationHeader) {
|
|
255
|
+
headers.Authorization = authorizationHeader;
|
|
256
|
+
}
|
|
257
|
+
const response = await this.fetchFn(this.graphqlApiUrl, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers,
|
|
260
|
+
body: JSON.stringify({ query, variables }),
|
|
261
|
+
});
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
const message = await githubErrorMessage(response, "POST", new URL(this.graphqlApiUrl));
|
|
264
|
+
throw new GitHubWorkTrackerProviderError(gitHubErrorMessageWithCredentialHint(message, response, authorizationHeader, this.config));
|
|
265
|
+
}
|
|
266
|
+
const payload = (await response.json());
|
|
267
|
+
if (payload.errors?.length) {
|
|
268
|
+
throw new GitHubWorkTrackerProviderError(`GitHub GraphQL request failed: ${payload.errors
|
|
269
|
+
.map((error) => error.message ?? "unknown error")
|
|
270
|
+
.join("; ")}`);
|
|
271
|
+
}
|
|
272
|
+
if (!payload.data) {
|
|
273
|
+
throw new GitHubWorkTrackerProviderError("GitHub GraphQL response did not include data");
|
|
274
|
+
}
|
|
275
|
+
return payload.data;
|
|
276
|
+
}
|
|
277
|
+
issueToWorkItem(issue) {
|
|
278
|
+
return {
|
|
279
|
+
id: `github-${issue.number}`,
|
|
280
|
+
title: requiredNonEmptyString(issue.title, "issue.title"),
|
|
281
|
+
description: issue.body ?? null,
|
|
282
|
+
status: workStatusFromIssue(issue),
|
|
283
|
+
provider: "github",
|
|
284
|
+
labels: userLabelNames(issue),
|
|
285
|
+
assignees: assigneeLogins(issue),
|
|
286
|
+
milestone: issue.milestone?.title ?? null,
|
|
287
|
+
createdAt: issue.created_at ?? null,
|
|
288
|
+
updatedAt: issue.updated_at ?? null,
|
|
289
|
+
closedAt: issue.closed_at ?? null,
|
|
290
|
+
webUrl: issue.html_url ?? null,
|
|
291
|
+
externalRef: this.issueExternalRef(issue),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
commentToWorkComment(comment, issueNumber) {
|
|
295
|
+
return {
|
|
296
|
+
id: `github-comment-${comment.id}`,
|
|
297
|
+
body: comment.body ?? "",
|
|
298
|
+
author: comment.user?.login ?? null,
|
|
299
|
+
createdAt: comment.created_at ?? null,
|
|
300
|
+
updatedAt: comment.updated_at ?? null,
|
|
301
|
+
externalRef: {
|
|
302
|
+
provider: "github",
|
|
303
|
+
host: this.config.host ?? null,
|
|
304
|
+
repositoryOwner: this.config.repository.owner,
|
|
305
|
+
repositoryName: this.config.repository.name,
|
|
306
|
+
itemId: String(comment.id),
|
|
307
|
+
itemNumber: issueNumber,
|
|
308
|
+
nodeId: comment.node_id ?? null,
|
|
309
|
+
webUrl: comment.html_url ?? null,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
issueExternalRef(issue) {
|
|
314
|
+
return {
|
|
315
|
+
provider: "github",
|
|
316
|
+
host: this.config.host ?? null,
|
|
317
|
+
repositoryOwner: this.config.repository.owner,
|
|
318
|
+
repositoryName: this.config.repository.name,
|
|
319
|
+
itemId: String(issue.number),
|
|
320
|
+
itemNumber: issue.number,
|
|
321
|
+
nodeId: issue.node_id ?? null,
|
|
322
|
+
webUrl: issue.html_url ?? null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
authorizationHeader() {
|
|
326
|
+
if (this.staticAuthorizationHeader) {
|
|
327
|
+
return this.staticAuthorizationHeader;
|
|
328
|
+
}
|
|
329
|
+
if (this.credentialAuthorizationHeader !== undefined) {
|
|
330
|
+
return this.credentialAuthorizationHeader ?? undefined;
|
|
331
|
+
}
|
|
332
|
+
if (!this.credentialRunner) {
|
|
333
|
+
this.credentialAuthorizationHeader = null;
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
const credential = fillGitCredential(this.credentialRunner, githubCredentialRequest(this.config), { interactive: this.credentialInteractive });
|
|
337
|
+
this.credentialAuthorizationHeader = credential
|
|
338
|
+
? authorizationHeaderFromCredential(credential)
|
|
339
|
+
: null;
|
|
340
|
+
return this.credentialAuthorizationHeader ?? undefined;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export function normalizeGitHubApiBaseUrl(hostOrApiBaseUrl) {
|
|
344
|
+
const value = hostOrApiBaseUrl?.trim();
|
|
345
|
+
if (!value || value === "github.com" || value === "https://github.com") {
|
|
346
|
+
return defaultGitHubApiBaseUrl;
|
|
347
|
+
}
|
|
348
|
+
if (value === "api.github.com" || value === "https://api.github.com") {
|
|
349
|
+
return defaultGitHubApiBaseUrl;
|
|
350
|
+
}
|
|
351
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
352
|
+
return value.replace(/\/+$/, "");
|
|
353
|
+
}
|
|
354
|
+
return `https://${value.replace(/\/+$/, "")}/api/v3`;
|
|
355
|
+
}
|
|
356
|
+
export function normalizeGitHubGraphQLApiUrl(hostOrApiBaseUrl) {
|
|
357
|
+
const value = hostOrApiBaseUrl?.trim();
|
|
358
|
+
if (!value ||
|
|
359
|
+
value === "github.com" ||
|
|
360
|
+
value === "https://github.com" ||
|
|
361
|
+
value === "api.github.com" ||
|
|
362
|
+
value === "https://api.github.com") {
|
|
363
|
+
return "https://api.github.com/graphql";
|
|
364
|
+
}
|
|
365
|
+
const url = value.startsWith("http://") || value.startsWith("https://")
|
|
366
|
+
? new URL(value)
|
|
367
|
+
: new URL(`https://${value}`);
|
|
368
|
+
if (url.hostname === "api.github.com") {
|
|
369
|
+
return "https://api.github.com/graphql";
|
|
370
|
+
}
|
|
371
|
+
if (url.pathname.endsWith("/api/graphql") || url.pathname.endsWith("/graphql")) {
|
|
372
|
+
return url.toString().replace(/\/+$/, "");
|
|
373
|
+
}
|
|
374
|
+
return `${url.protocol}//${url.host}/api/graphql`;
|
|
375
|
+
}
|
|
376
|
+
export function githubCredentialRequest(config) {
|
|
377
|
+
const host = normalizeGitHubCredentialHost(config.host);
|
|
378
|
+
return {
|
|
379
|
+
protocol: "https",
|
|
380
|
+
host,
|
|
381
|
+
path: `${config.repository.owner}/${config.repository.name}.git`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
export function normalizeGitHubCredentialHost(hostOrApiBaseUrl) {
|
|
385
|
+
const value = hostOrApiBaseUrl?.trim();
|
|
386
|
+
if (!value || value === "github.com" || value === "https://github.com") {
|
|
387
|
+
return "github.com";
|
|
388
|
+
}
|
|
389
|
+
if (value === "api.github.com" || value === "https://api.github.com") {
|
|
390
|
+
return "github.com";
|
|
391
|
+
}
|
|
392
|
+
const url = value.startsWith("http://") || value.startsWith("https://")
|
|
393
|
+
? new URL(value)
|
|
394
|
+
: new URL(`https://${value}`);
|
|
395
|
+
return url.host;
|
|
396
|
+
}
|
|
397
|
+
function githubProjectV2BoardConfig(config) {
|
|
398
|
+
const board = config.board;
|
|
399
|
+
if (!board || board.kind !== "github-project-v2") {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
const projectId = optionalNonEmptyString(board.projectId ?? board.id, "board.projectId");
|
|
403
|
+
if (!projectId) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
const statusFieldId = optionalNonEmptyString(board.statusFieldId, "board.statusFieldId") ??
|
|
407
|
+
undefined;
|
|
408
|
+
return {
|
|
409
|
+
projectId,
|
|
410
|
+
...(statusFieldId ? { statusFieldId } : {}),
|
|
411
|
+
...(board.statusOptions ? { statusOptions: board.statusOptions } : {}),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
export function defaultGitCredentialRunner(request, options) {
|
|
415
|
+
const result = spawnSync("git", ["credential", "fill"], {
|
|
416
|
+
input: gitCredentialInput(request),
|
|
417
|
+
encoding: "utf8",
|
|
418
|
+
env: {
|
|
419
|
+
...process.env,
|
|
420
|
+
...(options.interactive
|
|
421
|
+
? {}
|
|
422
|
+
: {
|
|
423
|
+
GCM_INTERACTIVE: "0",
|
|
424
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
425
|
+
}),
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
status: result.status,
|
|
430
|
+
stdout: result.stdout ?? "",
|
|
431
|
+
stderr: result.stderr ?? "",
|
|
432
|
+
...(result.error ? { error: result.error } : {}),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function fillGitCredential(runner, request, options) {
|
|
436
|
+
const result = runner(request, options);
|
|
437
|
+
if (result.status !== 0 || result.error) {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
const credential = parseGitCredentialOutput(result.stdout);
|
|
441
|
+
return credential.password || (credential.authtype && credential.credential)
|
|
442
|
+
? credential
|
|
443
|
+
: undefined;
|
|
444
|
+
}
|
|
445
|
+
function authorizationHeaderFromCredential(credential) {
|
|
446
|
+
const authtype = optionalNonEmptyString(credential.authtype, "authtype");
|
|
447
|
+
const encodedCredential = optionalNonEmptyString(credential.credential, "credential");
|
|
448
|
+
if (authtype && encodedCredential) {
|
|
449
|
+
return `${authtype} ${encodedCredential}`;
|
|
450
|
+
}
|
|
451
|
+
const password = optionalNonEmptyString(credential.password, "password");
|
|
452
|
+
return password ? `Bearer ${password}` : undefined;
|
|
453
|
+
}
|
|
454
|
+
function gitCredentialInput(request) {
|
|
455
|
+
return [
|
|
456
|
+
`protocol=${request.protocol}`,
|
|
457
|
+
`host=${request.host}`,
|
|
458
|
+
...(request.path ? [`path=${request.path}`] : []),
|
|
459
|
+
"",
|
|
460
|
+
].join("\n");
|
|
461
|
+
}
|
|
462
|
+
function parseGitCredentialOutput(output) {
|
|
463
|
+
const credential = {};
|
|
464
|
+
for (const line of output.split(/\r?\n/)) {
|
|
465
|
+
if (line.length === 0) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const separator = line.indexOf("=");
|
|
469
|
+
if (separator <= 0) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
credential[line.slice(0, separator)] = line.slice(separator + 1);
|
|
473
|
+
}
|
|
474
|
+
return credential;
|
|
475
|
+
}
|
|
476
|
+
function workStatusFromIssue(issue) {
|
|
477
|
+
if (issue.state === "closed") {
|
|
478
|
+
return issue.state_reason === "not_planned" ? "wont_do" : "done";
|
|
479
|
+
}
|
|
480
|
+
const statusLabel = labelNames(issue).find((label) => label.startsWith(githubStatusLabelPrefix));
|
|
481
|
+
if (!statusLabel) {
|
|
482
|
+
return "todo";
|
|
483
|
+
}
|
|
484
|
+
const candidate = statusLabel.slice(githubStatusLabelPrefix.length);
|
|
485
|
+
return workStatuses.has(candidate)
|
|
486
|
+
? candidate
|
|
487
|
+
: "todo";
|
|
488
|
+
}
|
|
489
|
+
function githubStateForStatus(status) {
|
|
490
|
+
assertWorkStatus(status);
|
|
491
|
+
return closedStatuses.has(status) ? "closed" : "open";
|
|
492
|
+
}
|
|
493
|
+
function githubStateForQuery(statuses) {
|
|
494
|
+
if (!statuses || statuses.size === 0) {
|
|
495
|
+
return "all";
|
|
496
|
+
}
|
|
497
|
+
let hasOpen = false;
|
|
498
|
+
let hasClosed = false;
|
|
499
|
+
for (const status of statuses) {
|
|
500
|
+
hasOpen ||= openStatuses.has(status);
|
|
501
|
+
hasClosed ||= closedStatuses.has(status);
|
|
502
|
+
}
|
|
503
|
+
return hasOpen && hasClosed ? "all" : hasClosed ? "closed" : "open";
|
|
504
|
+
}
|
|
505
|
+
function labelsWithStatus(labels, status) {
|
|
506
|
+
const normalized = normalizeStringArray(labels, "labels").filter((label) => !label.startsWith(githubStatusLabelPrefix));
|
|
507
|
+
if (status && openStatuses.has(status) && status !== "todo") {
|
|
508
|
+
normalized.push(`${githubStatusLabelPrefix}${status}`);
|
|
509
|
+
}
|
|
510
|
+
return dedupeStrings(normalized);
|
|
511
|
+
}
|
|
512
|
+
function userLabelNames(issue) {
|
|
513
|
+
return labelNames(issue).filter((label) => !label.startsWith(githubStatusLabelPrefix));
|
|
514
|
+
}
|
|
515
|
+
function labelNames(issue) {
|
|
516
|
+
return (issue.labels ?? [])
|
|
517
|
+
.map((label) => (typeof label === "string" ? label : label.name))
|
|
518
|
+
.filter((label) => Boolean(label && label.trim()))
|
|
519
|
+
.map((label) => label.trim());
|
|
520
|
+
}
|
|
521
|
+
function assigneeLogins(issue) {
|
|
522
|
+
return (issue.assignees ?? [])
|
|
523
|
+
.map((assignee) => assignee.login)
|
|
524
|
+
.filter((login) => Boolean(login && login.trim()))
|
|
525
|
+
.map((login) => login.trim());
|
|
526
|
+
}
|
|
527
|
+
function issueNumberFromRef(ref) {
|
|
528
|
+
if (ref.provider && ref.provider !== "github") {
|
|
529
|
+
throw new GitHubWorkTrackerProviderError(`github provider cannot resolve ${ref.provider} work item refs`);
|
|
530
|
+
}
|
|
531
|
+
if (ref.externalRef?.provider && ref.externalRef.provider !== "github") {
|
|
532
|
+
throw new GitHubWorkTrackerProviderError(`github provider cannot resolve ${ref.externalRef.provider} external refs`);
|
|
533
|
+
}
|
|
534
|
+
const candidate = ref.externalRef?.itemNumber ??
|
|
535
|
+
ref.id ??
|
|
536
|
+
ref.externalRef?.itemId;
|
|
537
|
+
if (candidate === undefined || candidate === null) {
|
|
538
|
+
throw new GitHubWorkTrackerProviderError("GitHub issue number is required");
|
|
539
|
+
}
|
|
540
|
+
if (typeof candidate === "number") {
|
|
541
|
+
return positiveInteger(candidate, "issue number");
|
|
542
|
+
}
|
|
543
|
+
const normalized = candidate.trim().replace(/^github-/, "");
|
|
544
|
+
return positiveInteger(Number(normalized), "issue number");
|
|
545
|
+
}
|
|
546
|
+
function normalizeStatusFilter(status) {
|
|
547
|
+
if (status === undefined) {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
const values = Array.isArray(status) ? status : [status];
|
|
551
|
+
const normalized = new Set();
|
|
552
|
+
for (const value of values) {
|
|
553
|
+
assertWorkStatus(value);
|
|
554
|
+
normalized.add(value);
|
|
555
|
+
}
|
|
556
|
+
return normalized;
|
|
557
|
+
}
|
|
558
|
+
function matchesStatusFilter(item, statuses) {
|
|
559
|
+
return !statuses || statuses.size === 0 || statuses.has(item.status);
|
|
560
|
+
}
|
|
561
|
+
function matchesStringFilter(itemValues, requiredValues) {
|
|
562
|
+
return requiredValues.every((value) => itemValues?.includes(value));
|
|
563
|
+
}
|
|
564
|
+
function matchesSearch(item, search) {
|
|
565
|
+
return [item.id, item.title, item.description ?? ""].some((value) => value.toLowerCase().includes(search));
|
|
566
|
+
}
|
|
567
|
+
function requestArray(key, values) {
|
|
568
|
+
return values.length > 0 ? { [key]: values } : {};
|
|
569
|
+
}
|
|
570
|
+
function requestMilestone(value) {
|
|
571
|
+
if (value === undefined) {
|
|
572
|
+
return {};
|
|
573
|
+
}
|
|
574
|
+
if (value === null) {
|
|
575
|
+
return { milestone: null };
|
|
576
|
+
}
|
|
577
|
+
const normalized = requiredNonEmptyString(value, "milestone");
|
|
578
|
+
const numeric = Number(normalized);
|
|
579
|
+
return Number.isInteger(numeric) && numeric > 0
|
|
580
|
+
? { milestone: numeric }
|
|
581
|
+
: { milestone: normalized };
|
|
582
|
+
}
|
|
583
|
+
function normalizeLimit(limit) {
|
|
584
|
+
if (limit === undefined) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
588
|
+
throw new GitHubWorkTrackerProviderError("limit must be a non-negative integer");
|
|
589
|
+
}
|
|
590
|
+
return limit;
|
|
591
|
+
}
|
|
592
|
+
function normalizeStringArray(values, pathName) {
|
|
593
|
+
if (values === undefined) {
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
if (!Array.isArray(values)) {
|
|
597
|
+
throw new GitHubWorkTrackerProviderError(`${pathName} must be an array`);
|
|
598
|
+
}
|
|
599
|
+
return dedupeStrings(values.map((value, index) => requiredNonEmptyString(value, `${pathName}[${index}]`)));
|
|
600
|
+
}
|
|
601
|
+
function dedupeStrings(values) {
|
|
602
|
+
const seen = new Set();
|
|
603
|
+
const result = [];
|
|
604
|
+
for (const value of values) {
|
|
605
|
+
if (!seen.has(value)) {
|
|
606
|
+
seen.add(value);
|
|
607
|
+
result.push(value);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
function assertWorkStatus(status) {
|
|
613
|
+
if (!workStatuses.has(status)) {
|
|
614
|
+
throw new GitHubWorkTrackerProviderError(`Invalid work status: ${status}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function positiveInteger(value, pathName) {
|
|
618
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
619
|
+
throw new GitHubWorkTrackerProviderError(`${pathName} must be a positive integer`);
|
|
620
|
+
}
|
|
621
|
+
return value;
|
|
622
|
+
}
|
|
623
|
+
function requiredNonEmptyString(value, pathName) {
|
|
624
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
625
|
+
throw new GitHubWorkTrackerProviderError(`${pathName} must be a non-empty string`);
|
|
626
|
+
}
|
|
627
|
+
return value.trim();
|
|
628
|
+
}
|
|
629
|
+
function optionalNonEmptyString(value, pathName) {
|
|
630
|
+
if (value === undefined || value === null) {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
if (value.trim().length === 0) {
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
return requiredNonEmptyString(value, pathName);
|
|
637
|
+
}
|
|
638
|
+
function encodePathSegment(value) {
|
|
639
|
+
return encodeURIComponent(requiredNonEmptyString(value, "path segment"));
|
|
640
|
+
}
|
|
641
|
+
async function githubErrorMessage(response, method, url) {
|
|
642
|
+
let detail;
|
|
643
|
+
try {
|
|
644
|
+
const body = (await response.json());
|
|
645
|
+
detail = body.message;
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
detail = await response.text().catch(() => undefined);
|
|
649
|
+
}
|
|
650
|
+
return [
|
|
651
|
+
`GitHub request failed: ${method} ${url.pathname} returned ${response.status}`,
|
|
652
|
+
detail ? `: ${detail}` : "",
|
|
653
|
+
].join("");
|
|
654
|
+
}
|
|
655
|
+
function gitHubErrorMessageWithCredentialHint(message, response, authorizationHeader, config) {
|
|
656
|
+
if (authorizationHeader ||
|
|
657
|
+
(response.status !== 401 && response.status !== 403)) {
|
|
658
|
+
return message;
|
|
659
|
+
}
|
|
660
|
+
return (`${message}. No GitHub token or git credential was available for ` +
|
|
661
|
+
`${normalizeGitHubCredentialHost(config.host)}. Configure GITHUB_TOKEN, ` +
|
|
662
|
+
"GH_TOKEN, or a git credential helper.");
|
|
663
|
+
}
|