@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.
Files changed (100) hide show
  1. package/README.md +677 -0
  2. package/dist/browserOpener.d.ts +9 -0
  3. package/dist/browserOpener.js +47 -0
  4. package/dist/cli.d.ts +18 -0
  5. package/dist/cli.js +2374 -0
  6. package/dist/gitWorktreeService.d.ts +57 -0
  7. package/dist/gitWorktreeService.js +157 -0
  8. package/dist/index.d.ts +47 -0
  9. package/dist/index.js +47 -0
  10. package/dist/nexusAgentMcpConfig.d.ts +30 -0
  11. package/dist/nexusAgentMcpConfig.js +228 -0
  12. package/dist/nexusAutomation.d.ts +103 -0
  13. package/dist/nexusAutomation.js +390 -0
  14. package/dist/nexusAutomationAgentLaunch.d.ts +148 -0
  15. package/dist/nexusAutomationAgentLaunch.js +855 -0
  16. package/dist/nexusAutomationAgentProfile.d.ts +39 -0
  17. package/dist/nexusAutomationAgentProfile.js +103 -0
  18. package/dist/nexusAutomationAgentSurface.d.ts +62 -0
  19. package/dist/nexusAutomationAgentSurface.js +90 -0
  20. package/dist/nexusAutomationCommandExecutor.d.ts +29 -0
  21. package/dist/nexusAutomationCommandExecutor.js +251 -0
  22. package/dist/nexusAutomationConfig.d.ts +114 -0
  23. package/dist/nexusAutomationConfig.js +547 -0
  24. package/dist/nexusAutomationEnqueue.d.ts +37 -0
  25. package/dist/nexusAutomationEnqueue.js +128 -0
  26. package/dist/nexusAutomationRunOnce.d.ts +91 -0
  27. package/dist/nexusAutomationRunOnce.js +586 -0
  28. package/dist/nexusAutomationScheduler.d.ts +50 -0
  29. package/dist/nexusAutomationScheduler.js +196 -0
  30. package/dist/nexusAutomationStatus.d.ts +55 -0
  31. package/dist/nexusAutomationStatus.js +462 -0
  32. package/dist/nexusAutomationTarget.d.ts +19 -0
  33. package/dist/nexusAutomationTarget.js +33 -0
  34. package/dist/nexusAutomationTargetCycle.d.ts +90 -0
  35. package/dist/nexusAutomationTargetCycle.js +282 -0
  36. package/dist/nexusAutomationTargetReport.d.ts +136 -0
  37. package/dist/nexusAutomationTargetReport.js +504 -0
  38. package/dist/nexusAutomationWorktreeSetup.d.ts +89 -0
  39. package/dist/nexusAutomationWorktreeSetup.js +661 -0
  40. package/dist/nexusCoordination.d.ts +198 -0
  41. package/dist/nexusCoordination.js +1018 -0
  42. package/dist/nexusExtension.d.ts +31 -0
  43. package/dist/nexusExtension.js +1 -0
  44. package/dist/nexusHomeConfig.d.ts +38 -0
  45. package/dist/nexusHomeConfig.js +133 -0
  46. package/dist/nexusMcpServer.d.ts +31 -0
  47. package/dist/nexusMcpServer.js +1036 -0
  48. package/dist/nexusPluginCapabilities.d.ts +197 -0
  49. package/dist/nexusPluginCapabilities.js +201 -0
  50. package/dist/nexusProjectConfig.d.ts +95 -0
  51. package/dist/nexusProjectConfig.js +880 -0
  52. package/dist/nexusProjectHomeService.d.ts +121 -0
  53. package/dist/nexusProjectHomeService.js +171 -0
  54. package/dist/nexusProjectLifecycle.d.ts +62 -0
  55. package/dist/nexusProjectLifecycle.js +205 -0
  56. package/dist/nexusProjectOperations.d.ts +101 -0
  57. package/dist/nexusProjectOperations.js +296 -0
  58. package/dist/nexusProjectRegistry.d.ts +42 -0
  59. package/dist/nexusProjectRegistry.js +91 -0
  60. package/dist/nexusProjectScaffold.d.ts +25 -0
  61. package/dist/nexusProjectScaffold.js +61 -0
  62. package/dist/nexusProjectTemplate.d.ts +34 -0
  63. package/dist/nexusProjectTemplate.js +354 -0
  64. package/dist/nexusSkills.d.ts +134 -0
  65. package/dist/nexusSkills.js +647 -0
  66. package/dist/nexusWorkerContextBundle.d.ts +142 -0
  67. package/dist/nexusWorkerContextBundle.js +375 -0
  68. package/dist/processSupervisor.d.ts +89 -0
  69. package/dist/processSupervisor.js +440 -0
  70. package/dist/vibeKanbanApi.d.ts +11 -0
  71. package/dist/vibeKanbanApi.js +14 -0
  72. package/dist/vibeKanbanAuth.d.ts +25 -0
  73. package/dist/vibeKanbanAuth.js +101 -0
  74. package/dist/vibeKanbanBoardAdapter.d.ts +36 -0
  75. package/dist/vibeKanbanBoardAdapter.js +196 -0
  76. package/dist/vibeKanbanMcpConfig.d.ts +36 -0
  77. package/dist/vibeKanbanMcpConfig.js +191 -0
  78. package/dist/vibeKanbanProjectAdapter.d.ts +39 -0
  79. package/dist/vibeKanbanProjectAdapter.js +113 -0
  80. package/dist/vibeKanbanWorkspaceSetup.d.ts +1 -0
  81. package/dist/vibeKanbanWorkspaceSetup.js +96 -0
  82. package/dist/workItemService.d.ts +60 -0
  83. package/dist/workItemService.js +163 -0
  84. package/dist/workTrackingGitHubProvider.d.ts +71 -0
  85. package/dist/workTrackingGitHubProvider.js +663 -0
  86. package/dist/workTrackingGitLabProvider.d.ts +62 -0
  87. package/dist/workTrackingGitLabProvider.js +523 -0
  88. package/dist/workTrackingJiraProvider.d.ts +67 -0
  89. package/dist/workTrackingJiraProvider.js +652 -0
  90. package/dist/workTrackingLocalProvider.d.ts +49 -0
  91. package/dist/workTrackingLocalProvider.js +463 -0
  92. package/dist/workTrackingProviderService.d.ts +21 -0
  93. package/dist/workTrackingProviderService.js +117 -0
  94. package/dist/workTrackingTypes.d.ts +202 -0
  95. package/dist/workTrackingTypes.js +1 -0
  96. package/dist/workTrackingVibeProvider.d.ts +35 -0
  97. package/dist/workTrackingVibeProvider.js +119 -0
  98. package/dist/worktreeExecutionMetadata.d.ts +76 -0
  99. package/dist/worktreeExecutionMetadata.js +239 -0
  100. package/package.json +37 -0
@@ -0,0 +1,652 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { spawnSync } from "node:child_process";
3
+ export const jiraStatusLabelPrefix = "status:";
4
+ export const jiraRestApiPath = "/rest/api/3";
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
+ const jiraIssueFields = [
17
+ "summary",
18
+ "description",
19
+ "status",
20
+ "labels",
21
+ "assignee",
22
+ "created",
23
+ "updated",
24
+ "resolutiondate",
25
+ "issuetype",
26
+ "project",
27
+ ];
28
+ export class JiraWorkTrackerProviderError extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = "JiraWorkTrackerProviderError";
32
+ }
33
+ }
34
+ export const jiraWorkTrackerCapabilities = {
35
+ createItem: true,
36
+ listItems: true,
37
+ getItem: true,
38
+ updateItem: true,
39
+ comment: true,
40
+ labels: true,
41
+ assignees: true,
42
+ milestones: false,
43
+ board: false,
44
+ boardStatus: false,
45
+ draftItems: false,
46
+ webhooks: false,
47
+ };
48
+ export function jiraWorkTrackerCapabilitiesForConfig(config) {
49
+ const hasWorkflowBoard = config.board?.kind === "jira-workflow";
50
+ return {
51
+ ...jiraWorkTrackerCapabilities,
52
+ board: hasWorkflowBoard,
53
+ boardStatus: hasWorkflowBoard && Object.keys(jiraStatusTransitionOptions(config)).length > 0,
54
+ };
55
+ }
56
+ export function createJiraWorkTrackerProvider(options) {
57
+ return new JiraWorkTrackerProvider(options);
58
+ }
59
+ export class JiraWorkTrackerProvider {
60
+ provider = "jira";
61
+ capabilities;
62
+ config;
63
+ fetchFn;
64
+ apiBaseUrl;
65
+ webBaseUrl;
66
+ staticAuthorizationHeader;
67
+ credentialRunner;
68
+ credentialInteractive;
69
+ credentialAuthorizationHeader;
70
+ constructor(options) {
71
+ this.config = options.config;
72
+ this.fetchFn = options.fetch ?? fetch;
73
+ this.apiBaseUrl = normalizeJiraApiBaseUrl(options.apiBaseUrl ?? options.config.host);
74
+ this.webBaseUrl = normalizeJiraWebBaseUrl(options.apiBaseUrl ?? options.config.host);
75
+ this.capabilities = jiraWorkTrackerCapabilitiesForConfig(options.config);
76
+ const env = options.env ?? process.env;
77
+ const token = optionalNonEmptyString(options.token, "token") ??
78
+ optionalNonEmptyString(env.JIRA_TOKEN, "JIRA_TOKEN");
79
+ const email = optionalNonEmptyString(options.email, "email") ??
80
+ optionalNonEmptyString(env.JIRA_EMAIL, "JIRA_EMAIL") ??
81
+ optionalNonEmptyString(env.ATLASSIAN_EMAIL, "ATLASSIAN_EMAIL");
82
+ const apiToken = optionalNonEmptyString(options.apiToken, "apiToken") ??
83
+ optionalNonEmptyString(env.JIRA_API_TOKEN, "JIRA_API_TOKEN") ??
84
+ optionalNonEmptyString(env.ATLASSIAN_API_TOKEN, "ATLASSIAN_API_TOKEN");
85
+ this.staticAuthorizationHeader = token
86
+ ? `Bearer ${token}`
87
+ : email && apiToken
88
+ ? basicAuthorizationHeader(email, apiToken)
89
+ : undefined;
90
+ this.credentialRunner =
91
+ options.credentialRunner === false
92
+ ? undefined
93
+ : options.credentialRunner ?? defaultJiraCredentialRunner;
94
+ this.credentialInteractive = options.credentialInteractive ?? false;
95
+ }
96
+ async createWorkItem(input) {
97
+ const status = input.status ?? "todo";
98
+ assertWorkStatus(status);
99
+ rejectUnsupportedMilestone(input.milestone);
100
+ const fields = {
101
+ project: { key: requiredNonEmptyString(this.config.projectKey, "projectKey") },
102
+ issuetype: {
103
+ name: optionalNonEmptyString(this.config.issueType, "issueType") ?? "Task",
104
+ },
105
+ summary: requiredNonEmptyString(input.title, "title"),
106
+ ...(input.description !== undefined
107
+ ? { description: adfFromPlainText(input.description) }
108
+ : {}),
109
+ ...requestLabels(labelsWithStatus(input.labels, status)),
110
+ ...requestAssignee(input.assignees),
111
+ };
112
+ const created = await this.requestJson("POST", "/issue", {
113
+ fields,
114
+ });
115
+ const issueKey = requiredNonEmptyString(created.key ?? created.id, "created.key");
116
+ await this.applyConfiguredTransition(issueKey, status);
117
+ return this.getWorkItem({ id: issueKey });
118
+ }
119
+ async listWorkItems(query = {}) {
120
+ const statuses = normalizeStatusFilter(query.status);
121
+ const limit = normalizeLimit(query.limit);
122
+ const labels = normalizeStringArray(query.labels, "labels");
123
+ const assignees = normalizeStringArray(query.assignees, "assignees");
124
+ const body = {
125
+ jql: jiraJqlForQuery(this.config.projectKey, labels, assignees),
126
+ fields: jiraIssueFields,
127
+ maxResults: limit ? Math.min(Math.max(limit, 1), 100) : 100,
128
+ };
129
+ const searchResult = await this.requestJson("POST", "/search/jql", body);
130
+ const search = query.search?.trim().toLowerCase();
131
+ const items = (searchResult.issues ?? [])
132
+ .map((issue) => this.issueToWorkItem(issue))
133
+ .filter((item) => matchesStatusFilter(item, statuses))
134
+ .filter((item) => matchesStringFilter(item.labels, labels))
135
+ .filter((item) => matchesStringFilter(item.assignees, assignees))
136
+ .filter((item) => !search || matchesSearch(item, search));
137
+ return limit === undefined ? items : items.slice(0, limit);
138
+ }
139
+ async getWorkItem(ref) {
140
+ return this.issueToWorkItem(await this.getIssue(ref));
141
+ }
142
+ async updateWorkItem(ref, patch) {
143
+ rejectUnsupportedMilestone(patch.milestone);
144
+ const issueKey = issueKeyFromRef(ref);
145
+ const fields = {};
146
+ if (patch.title !== undefined) {
147
+ fields.summary = requiredNonEmptyString(patch.title, "title");
148
+ }
149
+ if (patch.description !== undefined) {
150
+ fields.description = adfFromPlainText(patch.description);
151
+ }
152
+ if (patch.assignees !== undefined) {
153
+ Object.assign(fields, requestAssignee(patch.assignees));
154
+ }
155
+ if (patch.status !== undefined) {
156
+ assertWorkStatus(patch.status);
157
+ }
158
+ if (patch.labels !== undefined || patch.status !== undefined) {
159
+ const baseLabels = patch.labels !== undefined
160
+ ? normalizeStringArray(patch.labels, "labels")
161
+ : labelNames(await this.getIssue(ref));
162
+ Object.assign(fields, requestLabels(labelsWithStatus(baseLabels, patch.status)));
163
+ }
164
+ if (Object.keys(fields).length > 0) {
165
+ await this.requestJson("PUT", `/issue/${encodePathSegment(issueKey)}`, {
166
+ fields,
167
+ });
168
+ }
169
+ if (patch.status !== undefined) {
170
+ await this.applyConfiguredTransition(issueKey, patch.status);
171
+ }
172
+ return this.getWorkItem({ id: issueKey });
173
+ }
174
+ async addComment(ref, body) {
175
+ const issueKey = issueKeyFromRef(ref);
176
+ const comment = await this.requestJson("POST", `/issue/${encodePathSegment(issueKey)}/comment`, {
177
+ body: adfFromPlainText(requiredNonEmptyString(body, "body")),
178
+ });
179
+ return this.commentToWorkComment(comment, issueKey);
180
+ }
181
+ async setStatus(ref, status) {
182
+ return this.updateWorkItem(ref, { status });
183
+ }
184
+ async getIssue(ref) {
185
+ const issueKey = issueKeyFromRef(ref);
186
+ const params = new URLSearchParams({
187
+ fields: jiraIssueFields.join(","),
188
+ });
189
+ return this.requestJson("GET", `/issue/${encodePathSegment(issueKey)}?${params.toString()}`);
190
+ }
191
+ async applyConfiguredTransition(issueKey, status) {
192
+ const transitionId = jiraStatusTransitionOptions(this.config)[status];
193
+ if (!transitionId) {
194
+ return;
195
+ }
196
+ await this.requestJson("POST", `/issue/${encodePathSegment(issueKey)}/transitions`, {
197
+ transition: {
198
+ id: transitionId,
199
+ },
200
+ });
201
+ }
202
+ async requestJson(method, pathAndQuery, body) {
203
+ const url = new URL(pathAndQuery.replace(/^\/+/, ""), `${this.apiBaseUrl}/`);
204
+ const headers = {
205
+ Accept: "application/json",
206
+ "User-Agent": "dev-nexus",
207
+ };
208
+ if (body !== undefined) {
209
+ headers["Content-Type"] = "application/json";
210
+ }
211
+ const authorizationHeader = this.authorizationHeader();
212
+ if (authorizationHeader) {
213
+ headers.Authorization = authorizationHeader;
214
+ }
215
+ const response = await this.fetchFn(url, {
216
+ method,
217
+ headers,
218
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
219
+ });
220
+ if (!response.ok) {
221
+ const message = await jiraErrorMessage(response, method, url);
222
+ throw new JiraWorkTrackerProviderError(jiraErrorMessageWithCredentialHint(message, response, authorizationHeader, this.config));
223
+ }
224
+ if (response.status === 204) {
225
+ return undefined;
226
+ }
227
+ return (await response.json());
228
+ }
229
+ issueToWorkItem(issue) {
230
+ return {
231
+ id: `jira-${requiredNonEmptyString(issue.key, "issue.key")}`,
232
+ title: requiredNonEmptyString(issue.fields.summary, "issue.fields.summary"),
233
+ description: adfToPlainText(issue.fields.description),
234
+ status: workStatusFromIssue(issue),
235
+ provider: "jira",
236
+ labels: userLabelNames(issue),
237
+ assignees: assigneeAccountIds(issue),
238
+ milestone: null,
239
+ createdAt: issue.fields.created ?? null,
240
+ updatedAt: issue.fields.updated ?? null,
241
+ closedAt: issue.fields.resolutiondate ?? null,
242
+ webUrl: this.issueWebUrl(issue),
243
+ externalRef: this.issueExternalRef(issue),
244
+ };
245
+ }
246
+ commentToWorkComment(comment, issueKey) {
247
+ return {
248
+ id: `jira-comment-${requiredNonEmptyString(comment.id, "comment.id")}`,
249
+ body: adfToPlainText(comment.body) ?? "",
250
+ author: comment.author?.displayName ?? comment.author?.accountId ?? null,
251
+ createdAt: comment.created ?? null,
252
+ updatedAt: comment.updated ?? null,
253
+ externalRef: {
254
+ provider: "jira",
255
+ host: this.config.host ?? null,
256
+ projectId: this.config.projectKey,
257
+ itemId: comment.id,
258
+ itemKey: issueKey,
259
+ webUrl: comment.self ?? null,
260
+ },
261
+ };
262
+ }
263
+ issueExternalRef(issue) {
264
+ return {
265
+ provider: "jira",
266
+ host: this.config.host ?? null,
267
+ projectId: issue.fields.project?.key ?? this.config.projectKey,
268
+ itemId: issue.id,
269
+ itemKey: issue.key,
270
+ webUrl: this.issueWebUrl(issue),
271
+ };
272
+ }
273
+ issueWebUrl(issue) {
274
+ return `${this.webBaseUrl}/browse/${encodeURIComponent(issue.key)}`;
275
+ }
276
+ authorizationHeader() {
277
+ if (this.staticAuthorizationHeader) {
278
+ return this.staticAuthorizationHeader;
279
+ }
280
+ if (this.credentialAuthorizationHeader !== undefined) {
281
+ return this.credentialAuthorizationHeader ?? undefined;
282
+ }
283
+ if (!this.credentialRunner) {
284
+ this.credentialAuthorizationHeader = null;
285
+ return undefined;
286
+ }
287
+ const credential = fillJiraCredential(this.credentialRunner, jiraCredentialRequest(this.config), { interactive: this.credentialInteractive });
288
+ this.credentialAuthorizationHeader = credential
289
+ ? authorizationHeaderFromCredential(credential)
290
+ : null;
291
+ return this.credentialAuthorizationHeader ?? undefined;
292
+ }
293
+ }
294
+ export function normalizeJiraApiBaseUrl(hostOrApiBaseUrl) {
295
+ const url = jiraUrl(hostOrApiBaseUrl);
296
+ const normalizedPath = url.pathname.replace(/\/+$/, "");
297
+ if (normalizedPath.endsWith(jiraRestApiPath)) {
298
+ return `${url.protocol}//${url.host}${normalizedPath}`;
299
+ }
300
+ return `${url.protocol}//${url.host}${normalizedPath}${jiraRestApiPath}`;
301
+ }
302
+ export function normalizeJiraWebBaseUrl(hostOrApiBaseUrl) {
303
+ const url = jiraUrl(hostOrApiBaseUrl);
304
+ let normalizedPath = url.pathname.replace(/\/+$/, "");
305
+ if (normalizedPath.endsWith(jiraRestApiPath)) {
306
+ normalizedPath = normalizedPath.slice(0, -jiraRestApiPath.length);
307
+ }
308
+ return `${url.protocol}//${url.host}${normalizedPath}`;
309
+ }
310
+ export function jiraCredentialRequest(config) {
311
+ return {
312
+ protocol: "https",
313
+ host: normalizeJiraCredentialHost(config.host),
314
+ };
315
+ }
316
+ export function normalizeJiraCredentialHost(hostOrApiBaseUrl) {
317
+ return jiraUrl(hostOrApiBaseUrl).host;
318
+ }
319
+ export function defaultJiraCredentialRunner(request, options) {
320
+ const result = spawnSync("git", ["credential", "fill"], {
321
+ input: jiraCredentialInput(request),
322
+ encoding: "utf8",
323
+ env: {
324
+ ...process.env,
325
+ ...(options.interactive
326
+ ? {}
327
+ : {
328
+ GCM_INTERACTIVE: "0",
329
+ GIT_TERMINAL_PROMPT: "0",
330
+ }),
331
+ },
332
+ });
333
+ return {
334
+ status: result.status,
335
+ stdout: result.stdout ?? "",
336
+ stderr: result.stderr ?? "",
337
+ ...(result.error ? { error: result.error } : {}),
338
+ };
339
+ }
340
+ function jiraStatusTransitionOptions(config) {
341
+ const board = config.board;
342
+ if (!board || board.kind !== "jira-workflow" || !board.statusOptions) {
343
+ return {};
344
+ }
345
+ return board.statusOptions;
346
+ }
347
+ function fillJiraCredential(runner, request, options) {
348
+ const result = runner(request, options);
349
+ if (result.status !== 0 || result.error) {
350
+ return undefined;
351
+ }
352
+ const credential = parseJiraCredentialOutput(result.stdout);
353
+ return credential.authtype && credential.credential
354
+ ? credential
355
+ : credential.username && credential.password
356
+ ? credential
357
+ : undefined;
358
+ }
359
+ function authorizationHeaderFromCredential(credential) {
360
+ const authtype = optionalNonEmptyString(credential.authtype, "authtype");
361
+ const encodedCredential = optionalNonEmptyString(credential.credential, "credential");
362
+ if (authtype && encodedCredential) {
363
+ return `${authtype} ${encodedCredential}`;
364
+ }
365
+ const username = optionalNonEmptyString(credential.username, "username");
366
+ const password = optionalNonEmptyString(credential.password, "password");
367
+ return username && password
368
+ ? basicAuthorizationHeader(username, password)
369
+ : undefined;
370
+ }
371
+ function jiraCredentialInput(request) {
372
+ return [`protocol=${request.protocol}`, `host=${request.host}`, ""].join("\n");
373
+ }
374
+ function parseJiraCredentialOutput(output) {
375
+ const credential = {};
376
+ for (const line of output.split(/\r?\n/)) {
377
+ if (line.length === 0) {
378
+ continue;
379
+ }
380
+ const separator = line.indexOf("=");
381
+ if (separator <= 0) {
382
+ continue;
383
+ }
384
+ credential[line.slice(0, separator)] = line.slice(separator + 1);
385
+ }
386
+ return credential;
387
+ }
388
+ function workStatusFromIssue(issue) {
389
+ const statusLabel = labelNames(issue).find((label) => label.startsWith(jiraStatusLabelPrefix));
390
+ if (statusLabel) {
391
+ const candidate = statusLabel.slice(jiraStatusLabelPrefix.length);
392
+ if (workStatuses.has(candidate)) {
393
+ return candidate;
394
+ }
395
+ }
396
+ const statusName = issue.fields.status?.name?.trim().toLowerCase();
397
+ if (statusName === "ready") {
398
+ return "ready";
399
+ }
400
+ if (statusName === "in progress" || statusName === "in_progress") {
401
+ return "in_progress";
402
+ }
403
+ if (statusName === "blocked") {
404
+ return "blocked";
405
+ }
406
+ if (statusName === "won't do" || statusName === "wont do") {
407
+ return "wont_do";
408
+ }
409
+ const categoryKey = issue.fields.status?.statusCategory?.key;
410
+ if (categoryKey === "done") {
411
+ return "done";
412
+ }
413
+ if (categoryKey === "indeterminate") {
414
+ return "in_progress";
415
+ }
416
+ return "todo";
417
+ }
418
+ function labelsWithStatus(labels, status) {
419
+ const normalized = normalizeStringArray(labels, "labels").filter((label) => !label.startsWith(jiraStatusLabelPrefix));
420
+ if (status && status !== "todo") {
421
+ normalized.push(`${jiraStatusLabelPrefix}${status}`);
422
+ }
423
+ return dedupeStrings(normalized);
424
+ }
425
+ function userLabelNames(issue) {
426
+ return labelNames(issue).filter((label) => !label.startsWith(jiraStatusLabelPrefix));
427
+ }
428
+ function labelNames(issue) {
429
+ return (issue.fields.labels ?? [])
430
+ .filter((label) => Boolean(label && label.trim()))
431
+ .map((label) => label.trim());
432
+ }
433
+ function assigneeAccountIds(issue) {
434
+ const accountId = issue.fields.assignee?.accountId?.trim();
435
+ return accountId ? [accountId] : [];
436
+ }
437
+ function issueKeyFromRef(ref) {
438
+ if (ref.provider && ref.provider !== "jira") {
439
+ throw new JiraWorkTrackerProviderError(`jira provider cannot resolve ${ref.provider} work item refs`);
440
+ }
441
+ if (ref.externalRef?.provider && ref.externalRef.provider !== "jira") {
442
+ throw new JiraWorkTrackerProviderError(`jira provider cannot resolve ${ref.externalRef.provider} external refs`);
443
+ }
444
+ const candidate = ref.externalRef?.itemKey ?? ref.id ?? ref.externalRef?.itemId;
445
+ if (candidate === undefined || candidate === null) {
446
+ throw new JiraWorkTrackerProviderError("Jira issue key or id is required");
447
+ }
448
+ return requiredNonEmptyString(String(candidate), "issue key").replace(/^jira-/, "");
449
+ }
450
+ function normalizeStatusFilter(status) {
451
+ if (status === undefined) {
452
+ return undefined;
453
+ }
454
+ const values = Array.isArray(status) ? status : [status];
455
+ const normalized = new Set();
456
+ for (const value of values) {
457
+ assertWorkStatus(value);
458
+ normalized.add(value);
459
+ }
460
+ return normalized;
461
+ }
462
+ function matchesStatusFilter(item, statuses) {
463
+ return !statuses || statuses.size === 0 || statuses.has(item.status);
464
+ }
465
+ function matchesStringFilter(itemValues, requiredValues) {
466
+ return requiredValues.every((value) => itemValues?.includes(value));
467
+ }
468
+ function matchesSearch(item, search) {
469
+ return [
470
+ item.id,
471
+ item.externalRef?.itemKey ?? "",
472
+ item.title,
473
+ item.description ?? "",
474
+ ].some((value) => value.toLowerCase().includes(search));
475
+ }
476
+ function requestLabels(labels) {
477
+ return labels.length > 0 ? { labels } : {};
478
+ }
479
+ function requestAssignee(assignees) {
480
+ if (assignees === undefined) {
481
+ return {};
482
+ }
483
+ const normalized = normalizeStringArray(assignees, "assignees");
484
+ if (normalized.length > 1) {
485
+ throw new JiraWorkTrackerProviderError("Jira supports only one assignee account id per issue");
486
+ }
487
+ return {
488
+ assignee: normalized.length === 0 ? null : { accountId: normalized[0] },
489
+ };
490
+ }
491
+ function jiraJqlForQuery(projectKey, labels, assignees) {
492
+ const clauses = [`project = ${jqlString(projectKey)}`];
493
+ if (labels.length > 0) {
494
+ clauses.push(`labels in (${labels.map(jqlString).join(", ")})`);
495
+ }
496
+ if (assignees.length > 0) {
497
+ clauses.push(`assignee in (${assignees.map(jqlString).join(", ")})`);
498
+ }
499
+ return `${clauses.join(" AND ")} ORDER BY updated DESC`;
500
+ }
501
+ function jqlString(value) {
502
+ return `"${requiredNonEmptyString(value, "jql value").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
503
+ }
504
+ function adfFromPlainText(value) {
505
+ if (value === null || value === undefined) {
506
+ return null;
507
+ }
508
+ const lines = value.split(/\r?\n/);
509
+ return {
510
+ type: "doc",
511
+ version: 1,
512
+ content: (lines.length > 0 ? lines : [""]).map((line) => ({
513
+ type: "paragraph",
514
+ ...(line.length > 0
515
+ ? {
516
+ content: [
517
+ {
518
+ type: "text",
519
+ text: line,
520
+ },
521
+ ],
522
+ }
523
+ : {}),
524
+ })),
525
+ };
526
+ }
527
+ function adfToPlainText(value) {
528
+ if (value === null || value === undefined) {
529
+ return null;
530
+ }
531
+ if (typeof value === "string") {
532
+ return value;
533
+ }
534
+ return value.content.map(adfNodeText).join("\n");
535
+ }
536
+ function adfNodeText(node) {
537
+ if (typeof node.text === "string") {
538
+ return node.text;
539
+ }
540
+ if (node.type === "hardBreak") {
541
+ return "\n";
542
+ }
543
+ if (!node.content) {
544
+ return "";
545
+ }
546
+ return node.content.map(adfNodeText).join("");
547
+ }
548
+ function normalizeLimit(limit) {
549
+ if (limit === undefined) {
550
+ return undefined;
551
+ }
552
+ if (!Number.isInteger(limit) || limit < 0) {
553
+ throw new JiraWorkTrackerProviderError("limit must be a non-negative integer");
554
+ }
555
+ return limit;
556
+ }
557
+ function normalizeStringArray(values, pathName) {
558
+ if (values === undefined) {
559
+ return [];
560
+ }
561
+ if (!Array.isArray(values)) {
562
+ throw new JiraWorkTrackerProviderError(`${pathName} must be an array`);
563
+ }
564
+ return dedupeStrings(values.map((value, index) => requiredNonEmptyString(value, `${pathName}[${index}]`)));
565
+ }
566
+ function dedupeStrings(values) {
567
+ const seen = new Set();
568
+ const result = [];
569
+ for (const value of values) {
570
+ if (!seen.has(value)) {
571
+ seen.add(value);
572
+ result.push(value);
573
+ }
574
+ }
575
+ return result;
576
+ }
577
+ function assertWorkStatus(status) {
578
+ if (!workStatuses.has(status)) {
579
+ throw new JiraWorkTrackerProviderError(`Invalid work status: ${status}`);
580
+ }
581
+ }
582
+ function rejectUnsupportedMilestone(value) {
583
+ if (value !== undefined && value !== null) {
584
+ throw new JiraWorkTrackerProviderError("Jira milestone mapping is not configured; use Jira labels or issue fields directly");
585
+ }
586
+ }
587
+ function requiredNonEmptyString(value, pathName) {
588
+ if (typeof value !== "string" || value.trim().length === 0) {
589
+ throw new JiraWorkTrackerProviderError(`${pathName} must be a non-empty string`);
590
+ }
591
+ return value.trim();
592
+ }
593
+ function optionalNonEmptyString(value, pathName) {
594
+ if (value === undefined || value === null) {
595
+ return undefined;
596
+ }
597
+ if (value.trim().length === 0) {
598
+ return undefined;
599
+ }
600
+ return requiredNonEmptyString(value, pathName);
601
+ }
602
+ function encodePathSegment(value) {
603
+ return encodeURIComponent(requiredNonEmptyString(value, "path segment"));
604
+ }
605
+ function jiraUrl(hostOrApiBaseUrl) {
606
+ const value = optionalNonEmptyString(hostOrApiBaseUrl, "host");
607
+ if (!value) {
608
+ throw new JiraWorkTrackerProviderError("Jira provider requires a host or apiBaseUrl");
609
+ }
610
+ return value.startsWith("http://") || value.startsWith("https://")
611
+ ? new URL(value)
612
+ : new URL(`https://${value}`);
613
+ }
614
+ function basicAuthorizationHeader(username, password) {
615
+ return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
616
+ }
617
+ async function jiraErrorMessage(response, method, url) {
618
+ let detail;
619
+ try {
620
+ const body = (await response.json());
621
+ detail = jiraErrorDetail(body);
622
+ }
623
+ catch {
624
+ detail = await response.text().catch(() => undefined);
625
+ }
626
+ return [
627
+ `Jira request failed: ${method} ${url.pathname} returned ${response.status}`,
628
+ detail ? `: ${detail}` : "",
629
+ ].join("");
630
+ }
631
+ function jiraErrorDetail(body) {
632
+ if (body.message) {
633
+ return body.message;
634
+ }
635
+ if (body.errorMessages?.length) {
636
+ return body.errorMessages.join("; ");
637
+ }
638
+ if (body.errors && Object.keys(body.errors).length > 0) {
639
+ return JSON.stringify(body.errors);
640
+ }
641
+ return undefined;
642
+ }
643
+ function jiraErrorMessageWithCredentialHint(message, response, authorizationHeader, config) {
644
+ if (authorizationHeader ||
645
+ (response.status !== 401 && response.status !== 403)) {
646
+ return message;
647
+ }
648
+ return (`${message}. No Jira token, API token, or git credential was available ` +
649
+ `for ${normalizeJiraCredentialHost(config.host)}. Configure JIRA_TOKEN, ` +
650
+ "JIRA_EMAIL with JIRA_API_TOKEN, ATLASSIAN_EMAIL with " +
651
+ "ATLASSIAN_API_TOKEN, or a git credential helper.");
652
+ }
@@ -0,0 +1,49 @@
1
+ import type { CreateWorkItemInput, DetectedTracker, DetectTrackerInput, LocalWorkTrackingConfig, NexusProjectContext, TrackerCapabilities, TrackerProjectRef, WorkComment, WorkItem, WorkItemPatch, WorkItemQuery, WorkItemRef, WorkStatus, WorkTrackerProvider } from "./workTrackingTypes.js";
2
+ export declare const localWorkTrackingDirectoryName = ".dev-nexus";
3
+ export declare const localWorkTrackingStoreFileName = "work-items.json";
4
+ export declare const localWorkTrackingStoreVersion = 1;
5
+ export interface LocalWorkTrackingStore {
6
+ version: typeof localWorkTrackingStoreVersion;
7
+ nextNumber: number;
8
+ nextCommentNumber: number;
9
+ updatedAt: string;
10
+ items: WorkItem[];
11
+ comments: Record<string, WorkComment[]>;
12
+ }
13
+ export interface LocalWorkTrackerProviderOptions {
14
+ projectRoot?: string;
15
+ config?: LocalWorkTrackingConfig;
16
+ storePath?: string | null;
17
+ now?: () => Date | string;
18
+ }
19
+ export declare class LocalWorkTrackerProviderError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ export declare const localWorkTrackerCapabilities: TrackerCapabilities;
23
+ export declare function defaultLocalWorkTrackingStorePath(projectRoot: string): string;
24
+ export declare function resolveLocalWorkTrackingStorePath(projectRoot: string, config?: Pick<LocalWorkTrackingConfig, "storePath"> | string | null): string;
25
+ export declare function createLocalWorkTrackerProvider(options?: LocalWorkTrackerProviderOptions): LocalWorkTrackerProvider;
26
+ export declare class LocalWorkTrackerProvider implements WorkTrackerProvider {
27
+ readonly provider = "local";
28
+ readonly capabilities: TrackerCapabilities;
29
+ private readonly projectRoot?;
30
+ private readonly config?;
31
+ private readonly storePath?;
32
+ private readonly nowProvider;
33
+ constructor(options?: LocalWorkTrackerProviderOptions);
34
+ detect(input: DetectTrackerInput): Promise<DetectedTracker | undefined>;
35
+ ensureProject(context: NexusProjectContext): Promise<TrackerProjectRef>;
36
+ createWorkItem(input: CreateWorkItemInput): Promise<WorkItem>;
37
+ listWorkItems(query?: WorkItemQuery): Promise<WorkItem[]>;
38
+ getWorkItem(ref: WorkItemRef): Promise<WorkItem>;
39
+ updateWorkItem(ref: WorkItemRef, patch: WorkItemPatch): Promise<WorkItem>;
40
+ addComment(ref: WorkItemRef, body: string): Promise<WorkComment>;
41
+ setStatus(ref: WorkItemRef, status: WorkStatus): Promise<WorkItem>;
42
+ private resolveProjectRoot;
43
+ private storePathFor;
44
+ private loadStore;
45
+ private saveStore;
46
+ private now;
47
+ }
48
+ export declare function loadLocalWorkTrackingStore(storePath: string, timestamp?: string): LocalWorkTrackingStore;
49
+ export declare function saveLocalWorkTrackingStore(storePath: string, store: LocalWorkTrackingStore): void;