@enrichlayer/el-linear 1.10.0 → 1.15.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 +126 -10
- package/claude-skills/linear-operations/SKILL.md +41 -1
- package/dist/auth/linear-credential.d.ts +27 -0
- package/dist/auth/linear-credential.js +1 -0
- package/dist/auth/oauth-app-config.d.ts +4 -3
- package/dist/auth/oauth-app-config.js +13 -2
- package/dist/auth/oauth-callback.d.ts +2 -3
- package/dist/auth/oauth-callback.js +2 -2
- package/dist/auth/oauth-client.d.ts +8 -2
- package/dist/auth/oauth-client.js +26 -0
- package/dist/auth/oauth-fs.d.ts +2 -1
- package/dist/auth/oauth-headless.d.ts +2 -1
- package/dist/auth/oauth-storage.d.ts +5 -1
- package/dist/auth/oauth-storage.js +1 -1
- package/dist/auth/oauth-token.d.ts +4 -3
- package/dist/auth/oauth-token.js +16 -4
- package/dist/auth/token-resolver.d.ts +14 -5
- package/dist/auth/token-resolver.js +6 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +5 -5
- package/dist/commands/config.js +178 -5
- package/dist/commands/init/aliases.js +1 -1
- package/dist/commands/init/defaults.d.ts +2 -1
- package/dist/commands/init/index.js +45 -35
- package/dist/commands/init/oauth.d.ts +4 -1
- package/dist/commands/init/oauth.js +22 -4
- package/dist/commands/init/shared.d.ts +24 -2
- package/dist/commands/init/shared.js +35 -4
- package/dist/commands/init/token.d.ts +3 -3
- package/dist/commands/init/token.js +5 -24
- package/dist/commands/init/workspace.d.ts +2 -1
- package/dist/commands/init/workspace.js +1 -1
- package/dist/commands/introspect.d.ts +27 -0
- package/dist/commands/introspect.js +178 -0
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/relations.d.ts +3 -14
- package/dist/commands/issues/relations.js +3 -3
- package/dist/commands/issues.js +222 -43
- package/dist/commands/labels.js +2 -1
- package/dist/commands/profile.js +1 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +91 -7
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +67 -8
- package/dist/commands/search.js +30 -5
- package/dist/commands/users.js +4 -2
- package/dist/config/config.d.ts +99 -1
- package/dist/config/config.js +264 -52
- package/dist/config/error-enrichment.d.ts +62 -0
- package/dist/config/error-enrichment.js +417 -0
- package/dist/config/issue-validation.d.ts +37 -0
- package/dist/config/issue-validation.js +63 -1
- package/dist/config/paths.d.ts +2 -8
- package/dist/config/paths.js +4 -2
- package/dist/config/resolver.d.ts +8 -1
- package/dist/config/resolver.js +9 -2
- package/dist/main.js +13 -1
- package/dist/queries/comments-types.d.ts +15 -9
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +4 -3
- package/dist/queries/introspect-types.d.ts +8 -7
- package/dist/queries/issues-types.d.ts +92 -27
- package/dist/queries/issues.d.ts +49 -10
- package/dist/queries/issues.js +125 -5
- package/dist/queries/labels-types.d.ts +7 -6
- package/dist/queries/project-milestones-types.d.ts +5 -4
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +8 -7
- package/dist/queries/releases-types.d.ts +5 -4
- package/dist/queries/search-types.d.ts +28 -12
- package/dist/queries/templates-types.d.ts +3 -2
- package/dist/types/linear.d.ts +13 -1
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +1 -10
- package/dist/utils/extract-field.d.ts +19 -0
- package/dist/utils/extract-field.js +99 -0
- package/dist/utils/file-service.d.ts +6 -13
- package/dist/utils/file-service.js +0 -2
- package/dist/utils/formatters/summary.js +6 -1
- package/dist/utils/graphql-issues-service.d.ts +101 -45
- package/dist/utils/graphql-issues-service.js +252 -39
- package/dist/utils/graphql-service.d.ts +10 -12
- package/dist/utils/graphql-service.js +0 -3
- package/dist/utils/issue-reference-extractor.d.ts +7 -0
- package/dist/utils/issue-reference-extractor.js +5 -3
- package/dist/utils/issues-service-bootstrap.d.ts +28 -0
- package/dist/utils/issues-service-bootstrap.js +27 -0
- package/dist/utils/linear-service.d.ts +21 -14
- package/dist/utils/linear-service.js +73 -11
- package/dist/utils/markdown-prosemirror.js +12 -12
- package/dist/utils/mention-resolver.js +1 -1
- package/dist/utils/output.d.ts +81 -3
- package/dist/utils/output.js +61 -6
- package/dist/utils/project-slug.d.ts +21 -0
- package/dist/utils/project-slug.js +45 -0
- package/dist/utils/protected-ranges.d.ts +14 -0
- package/dist/utils/protected-ranges.js +88 -2
- package/dist/utils/sanitize-for-log.d.ts +24 -0
- package/dist/utils/sanitize-for-log.js +38 -0
- package/dist/utils/table-formatter.js +24 -0
- package/dist/utils/validators.d.ts +7 -2
- package/dist/utils/validators.js +6 -0
- package/dist/utils/workspace-url.js +20 -4
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveUserDisplayName } from "../config/resolver.js";
|
|
2
|
-
import { ARCHIVE_ISSUE_MUTATION, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
|
|
2
|
+
import { ARCHIVE_ISSUE_MUTATION, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_START_CONTEXT_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, TEAM_STARTED_STATUSES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
|
|
3
3
|
import { CREATE_LABEL_MUTATION } from "../queries/labels.js";
|
|
4
4
|
import { toISOStringOrNow } from "./date-format.js";
|
|
5
5
|
import { extractEmbeds } from "./embed-parser.js";
|
|
@@ -61,34 +61,81 @@ export class GraphQLIssuesService {
|
|
|
61
61
|
}
|
|
62
62
|
return this.transformIssueData(issueData);
|
|
63
63
|
}
|
|
64
|
+
async startIssue(issueId) {
|
|
65
|
+
const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
|
|
66
|
+
const context = await this.graphQLService.rawRequest(GET_ISSUE_START_CONTEXT_QUERY, { id: resolvedIssueId });
|
|
67
|
+
const issue = context.issue;
|
|
68
|
+
if (!issue) {
|
|
69
|
+
throw notFoundError("Issue", issueId);
|
|
70
|
+
}
|
|
71
|
+
const previousState = issue.state
|
|
72
|
+
? {
|
|
73
|
+
id: issue.state.id,
|
|
74
|
+
name: issue.state.name,
|
|
75
|
+
type: issue.state.type,
|
|
76
|
+
}
|
|
77
|
+
: undefined;
|
|
78
|
+
if (previousState &&
|
|
79
|
+
["started", "completed", "canceled"].includes(previousState.type)) {
|
|
80
|
+
return {
|
|
81
|
+
issue: await this.getIssueById(resolvedIssueId),
|
|
82
|
+
previousState,
|
|
83
|
+
started: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (!issue.team?.id) {
|
|
87
|
+
throw new Error(`Issue ${issue.identifier} has no team; cannot start it.`);
|
|
88
|
+
}
|
|
89
|
+
const statuses = await this.graphQLService.rawRequest(TEAM_STARTED_STATUSES_QUERY, { teamId: issue.team.id });
|
|
90
|
+
const startedStatus = statuses.team?.states.nodes
|
|
91
|
+
.slice()
|
|
92
|
+
.sort((a, b) => a.position - b.position)[0];
|
|
93
|
+
if (!startedStatus) {
|
|
94
|
+
throw new Error(`Team ${issue.team.key} has no workflow status of type "started".`);
|
|
95
|
+
}
|
|
96
|
+
const updated = await this.updateIssue({ id: resolvedIssueId, statusId: startedStatus.id }, "adding");
|
|
97
|
+
return {
|
|
98
|
+
issue: updated,
|
|
99
|
+
previousState,
|
|
100
|
+
started: true,
|
|
101
|
+
targetState: { id: startedStatus.id, name: startedStatus.name },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
64
104
|
async updateIssue(args, labelMode = "overwriting") {
|
|
65
|
-
|
|
105
|
+
// Normalize URL/slug-id --project inputs to UUIDs before the batch
|
|
106
|
+
// resolver runs; see comment on `withNormalizedProjectId`.
|
|
107
|
+
const normalizedArgs = await this.withNormalizedProjectId(args);
|
|
108
|
+
const { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult } = await this.resolveUpdateContext(normalizedArgs);
|
|
66
109
|
let teamIdForLabels = issueTeamId;
|
|
67
|
-
if (!teamIdForLabels &&
|
|
110
|
+
if (!teamIdForLabels && normalizedArgs.labelIds && resolvedIssueId) {
|
|
68
111
|
teamIdForLabels = await this.fetchIssueTeamId(resolvedIssueId);
|
|
69
112
|
}
|
|
70
|
-
const finalLabelIds = await this.resolveLabelsWithMode(
|
|
71
|
-
const finalProjectId =
|
|
72
|
-
? this.resolveProjectId(
|
|
113
|
+
const finalLabelIds = await this.resolveLabelsWithMode(normalizedArgs.labelIds, resolveResult, labelMode, currentIssueLabels, teamIdForLabels, normalizedArgs.teamId);
|
|
114
|
+
const finalProjectId = normalizedArgs.projectId
|
|
115
|
+
? this.resolveProjectId(normalizedArgs.projectId, resolveResult)
|
|
73
116
|
: undefined;
|
|
74
|
-
const { projectMilestoneNodes, issueProjectMilestoneNodes } = this.extractMilestoneNodes(
|
|
75
|
-
const finalMilestoneId = this.resolveMilestoneId(
|
|
76
|
-
const finalCycleId = await this.resolveCycleIdForUpdate(
|
|
77
|
-
const resolvedStatusId = await this.resolveStatusIdForUpdate(
|
|
117
|
+
const { projectMilestoneNodes, issueProjectMilestoneNodes } = this.extractMilestoneNodes(normalizedArgs, resolveResult);
|
|
118
|
+
const finalMilestoneId = this.resolveMilestoneId(normalizedArgs.milestoneId, resolveResult, projectMilestoneNodes, issueProjectMilestoneNodes);
|
|
119
|
+
const finalCycleId = await this.resolveCycleIdForUpdate(normalizedArgs, resolveResult, resolvedIssueId);
|
|
120
|
+
const resolvedStatusId = await this.resolveStatusIdForUpdate(normalizedArgs, resolvedIssueId);
|
|
78
121
|
// Resolve assignee in place — `buildUpdateInput` reads it
|
|
79
122
|
// directly. Match the createIssue pattern.
|
|
80
|
-
let assigneeId =
|
|
123
|
+
let assigneeId = normalizedArgs.assigneeId;
|
|
81
124
|
if (assigneeId && !isUuid(assigneeId)) {
|
|
82
125
|
assigneeId = await this.linearService.resolveUserId(assigneeId);
|
|
83
126
|
}
|
|
84
|
-
|
|
127
|
+
let delegateId = normalizedArgs.delegateId;
|
|
128
|
+
if (delegateId && !isUuid(delegateId)) {
|
|
129
|
+
delegateId = await this.linearService.resolveUserId(delegateId);
|
|
130
|
+
}
|
|
131
|
+
const updateInput = this.buildUpdateInput({ ...normalizedArgs, assigneeId, delegateId }, {
|
|
85
132
|
statusId: resolvedStatusId,
|
|
86
133
|
projectId: finalProjectId,
|
|
87
134
|
labelIds: finalLabelIds,
|
|
88
135
|
milestoneId: finalMilestoneId,
|
|
89
136
|
cycleId: finalCycleId,
|
|
90
137
|
});
|
|
91
|
-
return this.executeUpdateMutation(resolvedIssueId,
|
|
138
|
+
return this.executeUpdateMutation(resolvedIssueId, normalizedArgs.id, updateInput);
|
|
92
139
|
}
|
|
93
140
|
async archiveIssue(issueId) {
|
|
94
141
|
const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
|
|
@@ -154,7 +201,12 @@ export class GraphQLIssuesService {
|
|
|
154
201
|
return this.transformIssueData(issueUpdate.issue);
|
|
155
202
|
}
|
|
156
203
|
async createIssue(args) {
|
|
157
|
-
|
|
204
|
+
// Pre-resolve URL/slug-id forms of --project to a UUID so the batch
|
|
205
|
+
// resolver below (which uses a `name eqIgnoreCase` filter) can skip
|
|
206
|
+
// the project lookup entirely. Plain name inputs flow through unchanged
|
|
207
|
+
// and get resolved by the batch query in a single round-trip.
|
|
208
|
+
const normalizedArgs = await this.withNormalizedProjectId(args);
|
|
209
|
+
const resolveVariables = this.buildCreateResolveVariables(normalizedArgs);
|
|
158
210
|
let resolveResult = {};
|
|
159
211
|
if (Object.keys(resolveVariables).length > 0) {
|
|
160
212
|
const { __labelNames, ...queryVars } = resolveVariables;
|
|
@@ -165,23 +217,56 @@ export class GraphQLIssuesService {
|
|
|
165
217
|
batchPromise,
|
|
166
218
|
this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables),
|
|
167
219
|
]);
|
|
168
|
-
resolveResult = {
|
|
220
|
+
resolveResult = {
|
|
221
|
+
...this.foldCreateBatchResult(batch),
|
|
222
|
+
labels: labels.labels,
|
|
223
|
+
};
|
|
169
224
|
}
|
|
170
225
|
else {
|
|
171
|
-
resolveResult = await batchPromise;
|
|
226
|
+
resolveResult = this.foldCreateBatchResult(await batchPromise);
|
|
172
227
|
}
|
|
173
228
|
}
|
|
174
|
-
const resolved = await this.resolveCreateFields(
|
|
229
|
+
const resolved = await this.resolveCreateFields(normalizedArgs, resolveResult);
|
|
175
230
|
// Mutate-the-input feels gross but matches the existing pattern
|
|
176
231
|
// — `resolved` doesn't carry assigneeId, so the buildCreateInput
|
|
177
232
|
// step reads `args.assigneeId` directly. Resolve in place.
|
|
178
|
-
let assigneeId =
|
|
233
|
+
let assigneeId = normalizedArgs.assigneeId;
|
|
179
234
|
if (assigneeId && !isUuid(assigneeId)) {
|
|
180
235
|
assigneeId = await this.linearService.resolveUserId(assigneeId);
|
|
181
236
|
}
|
|
182
|
-
|
|
237
|
+
let delegateId = normalizedArgs.delegateId;
|
|
238
|
+
if (delegateId && !isUuid(delegateId)) {
|
|
239
|
+
delegateId = await this.linearService.resolveUserId(delegateId);
|
|
240
|
+
}
|
|
241
|
+
const createInput = this.buildCreateInput({ ...normalizedArgs, assigneeId, delegateId }, resolved);
|
|
183
242
|
return this.executeCreateMutation(createInput);
|
|
184
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Folds the `@include`-gated `projectsByName` / `projectsById` aliases
|
|
246
|
+
* from a create batch response into the single `projects` field the
|
|
247
|
+
* field-resolution helpers expect. At most one project alias is ever
|
|
248
|
+
* present, since a `--project` input is either a name or a UUID.
|
|
249
|
+
*/
|
|
250
|
+
foldCreateBatchResult(batch) {
|
|
251
|
+
return {
|
|
252
|
+
teams: batch.teams,
|
|
253
|
+
projects: batch.projectsByName ?? batch.projectsById,
|
|
254
|
+
milestones: batch.milestones,
|
|
255
|
+
parentIssues: batch.parentIssues,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Folds the `@include`-gated `projectsByName` / `projectsById` aliases
|
|
260
|
+
* from an update batch response into the single `projects` field the
|
|
261
|
+
* field-resolution helpers expect. See `foldCreateBatchResult`.
|
|
262
|
+
*/
|
|
263
|
+
foldUpdateBatchResult(batch) {
|
|
264
|
+
return {
|
|
265
|
+
projects: batch.projectsByName ?? batch.projectsById,
|
|
266
|
+
milestones: batch.milestones,
|
|
267
|
+
issues: batch.issues,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
185
270
|
async resolveCreateFields(args, resolveResult) {
|
|
186
271
|
let teamId = args.teamId
|
|
187
272
|
? await this.resolveTeamId(args.teamId, resolveResult)
|
|
@@ -239,14 +324,23 @@ export class GraphQLIssuesService {
|
|
|
239
324
|
if (args.teamId && !isUuid(args.teamId)) {
|
|
240
325
|
Object.assign(resolveVariables, this.buildResolveVariablesForTeam(args.teamId));
|
|
241
326
|
}
|
|
242
|
-
|
|
243
|
-
|
|
327
|
+
// Only the `id` arm carries a name/UUID for resolution; `none` and
|
|
328
|
+
// `undefined` skip the resolve.
|
|
329
|
+
const projectIdArg = args.project?.kind === "id" ? args.project.id : undefined;
|
|
330
|
+
if (projectIdArg && !isUuid(projectIdArg)) {
|
|
331
|
+
resolveVariables.projectName = projectIdArg;
|
|
332
|
+
resolveVariables.hasProjectName = true;
|
|
244
333
|
}
|
|
245
334
|
if (args.assigneeId &&
|
|
246
335
|
!isUuid(args.assigneeId) &&
|
|
247
336
|
args.assigneeId.includes("@")) {
|
|
248
337
|
resolveVariables.assigneeEmail = args.assigneeId;
|
|
249
338
|
}
|
|
339
|
+
if (args.delegateId &&
|
|
340
|
+
!isUuid(args.delegateId) &&
|
|
341
|
+
args.delegateId.includes("@")) {
|
|
342
|
+
resolveVariables.delegateEmail = args.delegateId;
|
|
343
|
+
}
|
|
250
344
|
let resolveResult = {};
|
|
251
345
|
if (Object.keys(resolveVariables).length > 0) {
|
|
252
346
|
resolveResult =
|
|
@@ -255,10 +349,24 @@ export class GraphQLIssuesService {
|
|
|
255
349
|
const finalTeamId = args.teamId
|
|
256
350
|
? await this.resolveTeamId(args.teamId, resolveResult)
|
|
257
351
|
: undefined;
|
|
258
|
-
const finalProjectId =
|
|
259
|
-
? this.resolveProjectId(
|
|
352
|
+
const finalProjectId = projectIdArg
|
|
353
|
+
? this.resolveProjectId(projectIdArg, resolveResult)
|
|
260
354
|
: undefined;
|
|
261
355
|
const finalAssigneeId = this.resolveAssigneeId(args.assigneeId, resolveResult);
|
|
356
|
+
const finalDelegateId = await this.resolveDelegateId(args.delegateId, resolveResult);
|
|
357
|
+
// Map the public discriminant back to the internal filter shape:
|
|
358
|
+
// `{ kind: "id", id }` → `{ kind: "id", id: <resolvedUuid> }`
|
|
359
|
+
// `{ kind: "none" }` → pass through
|
|
360
|
+
// undefined → undefined (filter off)
|
|
361
|
+
// Reused by both the full-text and structured search paths so
|
|
362
|
+
// `--no-project` (`{ kind: "none" }`) participates in both — pre-fix,
|
|
363
|
+
// the full-text branch silently dropped `noProject` because only
|
|
364
|
+
// the resolved `projectId` was forwarded.
|
|
365
|
+
const projectFilter = args.project?.kind === "id" && finalProjectId
|
|
366
|
+
? { kind: "id", id: finalProjectId }
|
|
367
|
+
: args.project?.kind === "none"
|
|
368
|
+
? { kind: "none" }
|
|
369
|
+
: undefined;
|
|
262
370
|
const limit = args.limit ?? 10;
|
|
263
371
|
if (args.query) {
|
|
264
372
|
const searchResult = await this.graphQLService.rawRequest(SEARCH_ISSUES_QUERY, {
|
|
@@ -273,7 +381,8 @@ export class GraphQLIssuesService {
|
|
|
273
381
|
return this.applySearchFilters(results, {
|
|
274
382
|
teamId: finalTeamId,
|
|
275
383
|
assigneeId: finalAssigneeId,
|
|
276
|
-
|
|
384
|
+
delegateId: finalDelegateId,
|
|
385
|
+
project: projectFilter,
|
|
277
386
|
status: args.status,
|
|
278
387
|
labelNames: args.labelNames,
|
|
279
388
|
priority: args.priority,
|
|
@@ -282,8 +391,8 @@ export class GraphQLIssuesService {
|
|
|
282
391
|
const filter = this.buildSearchFilter({
|
|
283
392
|
teamId: finalTeamId,
|
|
284
393
|
assigneeId: finalAssigneeId,
|
|
285
|
-
|
|
286
|
-
|
|
394
|
+
delegateId: finalDelegateId,
|
|
395
|
+
project: projectFilter,
|
|
287
396
|
status: args.status,
|
|
288
397
|
labelNames: args.labelNames,
|
|
289
398
|
priority: args.priority,
|
|
@@ -316,6 +425,18 @@ export class GraphQLIssuesService {
|
|
|
316
425
|
}
|
|
317
426
|
resolveProjectId(projectId, resolveResult) {
|
|
318
427
|
if (isUuid(projectId)) {
|
|
428
|
+
// The create/update batch queries fetch a `projectsById` block for a
|
|
429
|
+
// UUID `--project` (folded into `resolveResult.projects`). An empty
|
|
430
|
+
// `nodes` array means the block ran but matched nothing — i.e. the
|
|
431
|
+
// UUID doesn't exist. Fail clearly here instead of passing a dead
|
|
432
|
+
// UUID to the mutation, which would surface a vague server error.
|
|
433
|
+
// When no block was fetched (`projects` undefined — e.g. the search
|
|
434
|
+
// path has no `projectsById` arm) there's nothing to validate, so
|
|
435
|
+
// the UUID passes through unchanged.
|
|
436
|
+
const fetched = resolveResult.projects?.nodes;
|
|
437
|
+
if (fetched && fetched.length === 0) {
|
|
438
|
+
throw notFoundError("Project", projectId);
|
|
439
|
+
}
|
|
319
440
|
return projectId;
|
|
320
441
|
}
|
|
321
442
|
const projectNodes = resolveResult.projects?.nodes;
|
|
@@ -542,6 +663,9 @@ export class GraphQLIssuesService {
|
|
|
542
663
|
if (args.assigneeId) {
|
|
543
664
|
input.assigneeId = args.assigneeId;
|
|
544
665
|
}
|
|
666
|
+
if (args.delegateId) {
|
|
667
|
+
input.delegateId = args.delegateId;
|
|
668
|
+
}
|
|
545
669
|
if (args.priority !== undefined) {
|
|
546
670
|
input.priority = args.priority;
|
|
547
671
|
}
|
|
@@ -597,6 +721,9 @@ export class GraphQLIssuesService {
|
|
|
597
721
|
if (args.assigneeId !== undefined) {
|
|
598
722
|
input.assigneeId = args.assigneeId;
|
|
599
723
|
}
|
|
724
|
+
if (args.delegateId !== undefined) {
|
|
725
|
+
input.delegateId = args.delegateId;
|
|
726
|
+
}
|
|
600
727
|
if (resolved.projectId !== undefined) {
|
|
601
728
|
input.projectId = resolved.projectId;
|
|
602
729
|
}
|
|
@@ -635,8 +762,22 @@ export class GraphQLIssuesService {
|
|
|
635
762
|
if (filters.assigneeId) {
|
|
636
763
|
filtered = filtered.filter((issue) => issue.assignee?.id === filters.assigneeId);
|
|
637
764
|
}
|
|
638
|
-
if (filters.
|
|
639
|
-
filtered = filtered.filter((issue) => issue.
|
|
765
|
+
if (filters.delegateId) {
|
|
766
|
+
filtered = filtered.filter((issue) => issue.delegate?.id === filters.delegateId);
|
|
767
|
+
}
|
|
768
|
+
if (filters.project) {
|
|
769
|
+
switch (filters.project.kind) {
|
|
770
|
+
case "id": {
|
|
771
|
+
const id = filters.project.id;
|
|
772
|
+
filtered = filtered.filter((issue) => issue.project?.id === id);
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
case "none":
|
|
776
|
+
filtered = filtered.filter((issue) => !issue.project);
|
|
777
|
+
break;
|
|
778
|
+
default:
|
|
779
|
+
return filters.project;
|
|
780
|
+
}
|
|
640
781
|
}
|
|
641
782
|
if (filters.status && filters.status.length > 0) {
|
|
642
783
|
filtered = filtered.filter((issue) => filters.status.includes(issue.state?.name ?? ""));
|
|
@@ -661,11 +802,20 @@ export class GraphQLIssuesService {
|
|
|
661
802
|
if (filters.assigneeId) {
|
|
662
803
|
filter.assignee = { id: { eq: filters.assigneeId } };
|
|
663
804
|
}
|
|
664
|
-
if (filters.
|
|
665
|
-
filter.
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
805
|
+
if (filters.delegateId) {
|
|
806
|
+
filter.delegate = { id: { eq: filters.delegateId } };
|
|
807
|
+
}
|
|
808
|
+
if (filters.project) {
|
|
809
|
+
switch (filters.project.kind) {
|
|
810
|
+
case "id":
|
|
811
|
+
filter.project = { id: { eq: filters.project.id } };
|
|
812
|
+
break;
|
|
813
|
+
case "none":
|
|
814
|
+
filter.project = { null: true };
|
|
815
|
+
break;
|
|
816
|
+
default:
|
|
817
|
+
return filters.project;
|
|
818
|
+
}
|
|
669
819
|
}
|
|
670
820
|
if (filters.status && filters.status.length > 0) {
|
|
671
821
|
filter.state = { name: { in: filters.status } };
|
|
@@ -699,6 +849,19 @@ export class GraphQLIssuesService {
|
|
|
699
849
|
}
|
|
700
850
|
return userNodes[0].id;
|
|
701
851
|
}
|
|
852
|
+
async resolveDelegateId(delegateId, resolveResult) {
|
|
853
|
+
if (!delegateId || isUuid(delegateId)) {
|
|
854
|
+
return delegateId;
|
|
855
|
+
}
|
|
856
|
+
if (!delegateId.includes("@")) {
|
|
857
|
+
return this.linearService.resolveUserId(delegateId);
|
|
858
|
+
}
|
|
859
|
+
const userNodes = resolveResult.delegates?.nodes;
|
|
860
|
+
if (!userNodes?.length) {
|
|
861
|
+
throw notFoundError("Delegate", delegateId);
|
|
862
|
+
}
|
|
863
|
+
return userNodes[0].id;
|
|
864
|
+
}
|
|
702
865
|
async resolveUpdateContext(args) {
|
|
703
866
|
let resolvedIssueId = args.id;
|
|
704
867
|
let issueTeamId;
|
|
@@ -715,11 +878,23 @@ export class GraphQLIssuesService {
|
|
|
715
878
|
resolveVariables.__labelNames = nonUuidLabels;
|
|
716
879
|
}
|
|
717
880
|
}
|
|
718
|
-
if (args.projectId
|
|
719
|
-
|
|
881
|
+
if (args.projectId) {
|
|
882
|
+
// A UUID resolves by `id` so the project's milestones are still
|
|
883
|
+
// fetched for `--project-milestone` name resolution; a name
|
|
884
|
+
// resolves by `name`. The `has*` flags gate the `@include`d
|
|
885
|
+
// blocks so an unset input never triggers a null no-op filter.
|
|
886
|
+
if (isUuid(args.projectId)) {
|
|
887
|
+
resolveVariables.projectId = args.projectId;
|
|
888
|
+
resolveVariables.hasProjectId = true;
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
resolveVariables.projectName = args.projectId;
|
|
892
|
+
resolveVariables.hasProjectName = true;
|
|
893
|
+
}
|
|
720
894
|
}
|
|
721
895
|
if (args.milestoneId && !isUuid(args.milestoneId)) {
|
|
722
896
|
resolveVariables.milestoneName = args.milestoneId;
|
|
897
|
+
resolveVariables.hasMilestoneName = true;
|
|
723
898
|
}
|
|
724
899
|
const { __labelNames, ...updateQueryVars } = resolveVariables;
|
|
725
900
|
const batchPromise = this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_UPDATE_QUERY, updateQueryVars);
|
|
@@ -730,10 +905,13 @@ export class GraphQLIssuesService {
|
|
|
730
905
|
batchPromise,
|
|
731
906
|
this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables),
|
|
732
907
|
]);
|
|
733
|
-
resolveResult = {
|
|
908
|
+
resolveResult = {
|
|
909
|
+
...this.foldUpdateBatchResult(batch),
|
|
910
|
+
labels: labels.labels,
|
|
911
|
+
};
|
|
734
912
|
}
|
|
735
913
|
else {
|
|
736
|
-
resolveResult = await batchPromise;
|
|
914
|
+
resolveResult = this.foldUpdateBatchResult(await batchPromise);
|
|
737
915
|
}
|
|
738
916
|
if (!isUuid(args.id)) {
|
|
739
917
|
const resolvedIssueNodes = resolveResult.issues?.nodes ?? [];
|
|
@@ -769,16 +947,44 @@ export class GraphQLIssuesService {
|
|
|
769
947
|
}
|
|
770
948
|
return this.linearService.resolveStatusId(args.statusId, teamId);
|
|
771
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Pre-resolve URL/slug-id project inputs to UUIDs before the batch
|
|
952
|
+
* resolver runs. Plain names and UUIDs pass through unchanged; only
|
|
953
|
+
* URL/slug-id forms incur a separate slug→UUID round-trip (the batch
|
|
954
|
+
* query's `name eqIgnoreCase` filter can't match a URL string).
|
|
955
|
+
*/
|
|
956
|
+
async withNormalizedProjectId(args) {
|
|
957
|
+
if (!args.projectId) {
|
|
958
|
+
return args;
|
|
959
|
+
}
|
|
960
|
+
const normalized = await this.linearService.normalizeProjectInput(args.projectId);
|
|
961
|
+
if (normalized === args.projectId) {
|
|
962
|
+
return args;
|
|
963
|
+
}
|
|
964
|
+
return { ...args, projectId: normalized };
|
|
965
|
+
}
|
|
772
966
|
buildCreateResolveVariables(args) {
|
|
773
967
|
const resolveVariables = {};
|
|
774
968
|
if (args.teamId && !isUuid(args.teamId)) {
|
|
775
969
|
Object.assign(resolveVariables, this.buildResolveVariablesForTeam(args.teamId));
|
|
776
970
|
}
|
|
777
|
-
if (args.projectId
|
|
778
|
-
|
|
971
|
+
if (args.projectId) {
|
|
972
|
+
// A UUID is resolved by `id` so the project's team associations
|
|
973
|
+
// are still fetched for team-project validation; a name is
|
|
974
|
+
// resolved by `name`. The `has*` flags gate the `@include`d
|
|
975
|
+
// blocks so an unset input never triggers a null no-op filter.
|
|
976
|
+
if (isUuid(args.projectId)) {
|
|
977
|
+
resolveVariables.projectId = args.projectId;
|
|
978
|
+
resolveVariables.hasProjectId = true;
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
resolveVariables.projectName = args.projectId;
|
|
982
|
+
resolveVariables.hasProjectName = true;
|
|
983
|
+
}
|
|
779
984
|
}
|
|
780
985
|
if (args.milestoneId && !isUuid(args.milestoneId)) {
|
|
781
986
|
resolveVariables.milestoneName = args.milestoneId;
|
|
987
|
+
resolveVariables.hasMilestoneName = true;
|
|
782
988
|
}
|
|
783
989
|
if (args.labelIds && args.labelIds.length > 0) {
|
|
784
990
|
const nonUuidLabels = args.labelIds.filter((id) => !isUuid(id));
|
|
@@ -852,6 +1058,13 @@ export class GraphQLIssuesService {
|
|
|
852
1058
|
url: issue.assignee.url ?? undefined,
|
|
853
1059
|
}
|
|
854
1060
|
: undefined,
|
|
1061
|
+
delegate: issue.delegate
|
|
1062
|
+
? {
|
|
1063
|
+
id: issue.delegate.id,
|
|
1064
|
+
name: resolveUserDisplayName(issue.delegate.id, issue.delegate.name),
|
|
1065
|
+
url: issue.delegate.url ?? undefined,
|
|
1066
|
+
}
|
|
1067
|
+
: undefined,
|
|
855
1068
|
team: issue.team
|
|
856
1069
|
? {
|
|
857
1070
|
id: issue.team.id,
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
+
import type { LinearCredential } from "../auth/linear-credential.js";
|
|
1
2
|
import type { GraphQLResponseData, GraphQLVariables } from "../types/linear.js";
|
|
2
3
|
import type { AuthOptions } from "./auth.js";
|
|
3
4
|
/**
|
|
4
|
-
* Constructor arg
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Constructor arg for `GraphQLService`. Re-exported alias of the shared
|
|
6
|
+
* `LinearCredential` union (`{ apiKey } | { oauthToken }`). Kept as a
|
|
7
|
+
* named local export so call sites that already import
|
|
8
|
+
* `GraphQLServiceAuth` keep compiling — the alias collapses through to
|
|
9
|
+
* the shared shape.
|
|
9
10
|
*
|
|
10
|
-
* The string
|
|
11
|
-
*
|
|
11
|
+
* The bare-string legacy arm was dropped in DEV-4068 T7. Tests now
|
|
12
|
+
* construct with `{ apiKey: "test-token" }` (mechanical rewrite, no
|
|
13
|
+
* semantic change — same `Authorization: <token>` header is emitted).
|
|
12
14
|
*/
|
|
13
|
-
export type GraphQLServiceAuth =
|
|
14
|
-
apiKey: string;
|
|
15
|
-
} | {
|
|
16
|
-
oauthToken: string;
|
|
17
|
-
};
|
|
15
|
+
export type GraphQLServiceAuth = LinearCredential;
|
|
18
16
|
export declare class GraphQLService {
|
|
19
17
|
private readonly graphQLClient;
|
|
20
18
|
constructor(auth: GraphQLServiceAuth);
|
|
@@ -2,9 +2,6 @@ import { LinearClient } from "@linear/sdk";
|
|
|
2
2
|
import { getActiveAuth } from "../auth/token-resolver.js";
|
|
3
3
|
function buildLinearClient(auth) {
|
|
4
4
|
const baseHeaders = { "public-file-urls-expire-in": "3600" };
|
|
5
|
-
if (typeof auth === "string") {
|
|
6
|
-
return new LinearClient({ apiKey: auth, headers: baseHeaders });
|
|
7
|
-
}
|
|
8
5
|
if ("oauthToken" in auth) {
|
|
9
6
|
// Linear's SDK natively supports OAuth via the `accessToken` option,
|
|
10
7
|
// which causes the underlying graphql-request client to send
|
|
@@ -5,6 +5,13 @@ export interface IssueReference {
|
|
|
5
5
|
reverse: boolean;
|
|
6
6
|
type: IssueRelationType;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Specificity ranking — used when the same identifier appears more than once
|
|
10
|
+
* in the text with different qualifiers. The strongest non-default inference
|
|
11
|
+
* wins. Exported because `auto-link-references` does the same merge over its
|
|
12
|
+
* own description+comments fan-in (DEV-4070 deduplication).
|
|
13
|
+
*/
|
|
14
|
+
export declare function specificity(ref: IssueReference): number;
|
|
8
15
|
/**
|
|
9
16
|
* Extract issue identifiers from text along with the inferred relation type.
|
|
10
17
|
*
|
|
@@ -45,10 +45,12 @@ function inferRelation(textBefore) {
|
|
|
45
45
|
return { type: "related", reverse: false };
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* Specificity ranking — used when the same identifier appears more than once
|
|
49
|
-
* with different qualifiers. The strongest non-default inference
|
|
48
|
+
* Specificity ranking — used when the same identifier appears more than once
|
|
49
|
+
* in the text with different qualifiers. The strongest non-default inference
|
|
50
|
+
* wins. Exported because `auto-link-references` does the same merge over its
|
|
51
|
+
* own description+comments fan-in (DEV-4070 deduplication).
|
|
50
52
|
*/
|
|
51
|
-
function specificity(ref) {
|
|
53
|
+
export function specificity(ref) {
|
|
52
54
|
if (ref.type === "duplicate") {
|
|
53
55
|
return 3;
|
|
54
56
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-trio bootstrap helper.
|
|
3
|
+
*
|
|
4
|
+
* The 3-line incantation
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const graphQLService = await createGraphQLService(rootOpts);
|
|
8
|
+
* const linearService = await createLinearService(rootOpts);
|
|
9
|
+
* const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* repeats verbatim across ~13 issue/batch command handlers. This helper
|
|
13
|
+
* collapses it to a single line; callers destructure only what they use.
|
|
14
|
+
*
|
|
15
|
+
* Sites that need just `graphQLService` + `linearService` (no `issuesService`)
|
|
16
|
+
* can keep their direct calls — there's no benefit to routing through a
|
|
17
|
+
* helper that constructs an unused third service.
|
|
18
|
+
*/
|
|
19
|
+
import type { AuthOptions } from "./auth.js";
|
|
20
|
+
import { GraphQLIssuesService } from "./graphql-issues-service.js";
|
|
21
|
+
import { type GraphQLService } from "./graphql-service.js";
|
|
22
|
+
import { type LinearService } from "./linear-service.js";
|
|
23
|
+
export interface IssuesServiceTrio {
|
|
24
|
+
graphQLService: GraphQLService;
|
|
25
|
+
linearService: LinearService;
|
|
26
|
+
issuesService: GraphQLIssuesService;
|
|
27
|
+
}
|
|
28
|
+
export declare function createIssuesService(options: AuthOptions): Promise<IssuesServiceTrio>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-trio bootstrap helper.
|
|
3
|
+
*
|
|
4
|
+
* The 3-line incantation
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const graphQLService = await createGraphQLService(rootOpts);
|
|
8
|
+
* const linearService = await createLinearService(rootOpts);
|
|
9
|
+
* const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* repeats verbatim across ~13 issue/batch command handlers. This helper
|
|
13
|
+
* collapses it to a single line; callers destructure only what they use.
|
|
14
|
+
*
|
|
15
|
+
* Sites that need just `graphQLService` + `linearService` (no `issuesService`)
|
|
16
|
+
* can keep their direct calls — there's no benefit to routing through a
|
|
17
|
+
* helper that constructs an unused third service.
|
|
18
|
+
*/
|
|
19
|
+
import { GraphQLIssuesService } from "./graphql-issues-service.js";
|
|
20
|
+
import { createGraphQLService, } from "./graphql-service.js";
|
|
21
|
+
import { createLinearService } from "./linear-service.js";
|
|
22
|
+
export async function createIssuesService(options) {
|
|
23
|
+
const graphQLService = await createGraphQLService(options);
|
|
24
|
+
const linearService = await createLinearService(options);
|
|
25
|
+
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
26
|
+
return { graphQLService, linearService, issuesService };
|
|
27
|
+
}
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
+
import type { LinearCredential } from "../auth/linear-credential.js";
|
|
1
2
|
import type { LinearComment, LinearCycleDetail, LinearCycleSummary, LinearLabel, LinearProject, LinearTeam, LinearUser } from "../types/linear.js";
|
|
2
3
|
import type { AuthOptions } from "./auth.js";
|
|
3
4
|
/**
|
|
4
|
-
* Constructor arg
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `Authorization: Bearer <token>` via the SDK's accessToken option).
|
|
9
|
-
*
|
|
10
|
-
* The string variant exists because hundreds of call sites and tests pass
|
|
11
|
-
* a plain string. We continue to support it indefinitely.
|
|
5
|
+
* Constructor arg for `LinearService`. Re-exported alias of the shared
|
|
6
|
+
* `LinearCredential` union (`{ apiKey } | { oauthToken }`). See
|
|
7
|
+
* `src/auth/linear-credential.ts` for the contract. The bare-string
|
|
8
|
+
* legacy arm was dropped in DEV-4068 T7.
|
|
12
9
|
*/
|
|
13
|
-
export type LinearServiceAuth =
|
|
14
|
-
apiKey: string;
|
|
15
|
-
} | {
|
|
16
|
-
oauthToken: string;
|
|
17
|
-
};
|
|
10
|
+
export type LinearServiceAuth = LinearCredential;
|
|
18
11
|
export declare class LinearService {
|
|
19
12
|
private readonly client;
|
|
20
13
|
constructor(auth: LinearServiceAuth);
|
|
@@ -26,6 +19,8 @@ export declare class LinearService {
|
|
|
26
19
|
nameFilter?: string;
|
|
27
20
|
states?: string[];
|
|
28
21
|
excludeStates?: string[];
|
|
22
|
+
/** Resolved team UUID — applied as a server-side filter before pagination. */
|
|
23
|
+
teamId?: string;
|
|
29
24
|
}): Promise<LinearProject[]>;
|
|
30
25
|
resolveTeamId(teamKeyOrNameOrId: string): Promise<string>;
|
|
31
26
|
resolveStatusId(statusName: string, teamId?: string): Promise<string>;
|
|
@@ -40,6 +35,18 @@ export declare class LinearService {
|
|
|
40
35
|
getCycles(teamFilter?: string, activeOnly?: boolean, limit?: number): Promise<LinearCycleSummary[]>;
|
|
41
36
|
getCycleById(cycleId: string, issuesLimit?: number): Promise<LinearCycleDetail>;
|
|
42
37
|
resolveCycleId(cycleNameOrId: string, teamFilter?: string): Promise<string>;
|
|
43
|
-
resolveProjectId(
|
|
38
|
+
resolveProjectId(projectInput: string): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Normalize a user-supplied project input to a UUID when the input is
|
|
41
|
+
* a URL or slug-id form. Pass-through for UUIDs and plain names — the
|
|
42
|
+
* latter stays a name so downstream batch-resolve queries can fold the
|
|
43
|
+
* lookup into their single round-trip.
|
|
44
|
+
*
|
|
45
|
+
* Used by callers that route project resolution through a separate
|
|
46
|
+
* batch-resolve step (e.g. `GraphqlIssuesService.createIssue`) — they
|
|
47
|
+
* can pre-normalize URL/slug inputs to UUIDs so the batch query's
|
|
48
|
+
* `name eqIgnoreCase` filter doesn't have to learn the URL/slug shape.
|
|
49
|
+
*/
|
|
50
|
+
normalizeProjectInput(projectInput: string): Promise<string>;
|
|
44
51
|
}
|
|
45
52
|
export declare function createLinearService(options: AuthOptions): Promise<LinearService>;
|