@enrichlayer/el-linear 1.44.2 → 1.46.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 +159 -0
- package/claude-skills/linear-operations/SKILL.md +17 -0
- package/dist/auth/oauth-storage.d.ts +5 -3
- package/dist/auth/oauth-token.d.ts +7 -0
- package/dist/auth/oauth-token.js +13 -0
- package/dist/auth/token-resolver.d.ts +2 -0
- package/dist/auth/token-resolver.js +25 -10
- package/dist/commands/init/index.js +17 -1
- package/dist/commands/init/oauth.d.ts +9 -1
- package/dist/commands/init/oauth.js +62 -3
- package/dist/commands/issues.js +244 -1
- package/dist/config/config.d.ts +30 -0
- package/dist/config/config.js +3 -0
- package/dist/config/consent-receipt.d.ts +76 -0
- package/dist/config/consent-receipt.js +219 -0
- package/dist/config/label-advisor.d.ts +53 -0
- package/dist/config/label-advisor.js +245 -0
- package/dist/queries/catalog-types.d.ts +129 -0
- package/dist/queries/catalog-types.js +1 -0
- package/dist/queries/catalog.d.ts +20 -0
- package/dist/queries/catalog.js +140 -0
- package/dist/utils/disk-cache.js +48 -14
- package/dist/utils/graphql-service.d.ts +42 -1
- package/dist/utils/graphql-service.js +267 -14
- package/dist/utils/linear-graphql-error.d.ts +100 -0
- package/dist/utils/linear-graphql-error.js +182 -0
- package/dist/utils/linear-service.d.ts +19 -2
- package/dist/utils/linear-service.js +194 -214
- package/dist/utils/output.d.ts +39 -0
- package/dist/utils/output.js +152 -1
- package/dist/utils/rate-limit-admission.d.ts +33 -0
- package/dist/utils/rate-limit-admission.js +240 -0
- package/package.json +79 -79
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { LinearClient } from "@linear/sdk";
|
|
2
2
|
import { getActiveAuth } from "../auth/token-resolver.js";
|
|
3
3
|
import { resolveUserDisplayName } from "../config/resolver.js";
|
|
4
|
+
import { GET_CYCLE_DETAIL_QUERY, GET_CYCLES_CATALOG_QUERY, GET_LABELS_CATALOG_QUERY, GET_PROJECTS_CATALOG_QUERY, RESOLVE_PROJECT_QUERY, } from "../queries/catalog.js";
|
|
4
5
|
import { toISOStringOrNow, toISOStringOrUndefined } from "./date-format.js";
|
|
5
6
|
import { multipleMatchesError, notFoundError } from "./error-messages.js";
|
|
7
|
+
import { createClientCredentialsTokenRenewal, GraphQLService, instrumentLinearClient, } from "./graphql-service.js";
|
|
6
8
|
import { parseIssueIdentifier } from "./identifier-parser.js";
|
|
9
|
+
import { graphQLErrorHttpStatus, toLinearGraphQLError, } from "./linear-graphql-error.js";
|
|
7
10
|
import { parseProjectSlugId } from "./project-slug.js";
|
|
11
|
+
import { createOAuthProfileRateLimitAdmission, RateLimitAdmissionRefusal, } from "./rate-limit-admission.js";
|
|
8
12
|
import { isUuid } from "./uuid.js";
|
|
9
13
|
const DEFAULT_CYCLE_PAGINATION_LIMIT = 250;
|
|
10
|
-
// The Linear SDK types don't accept string orderBy values, but the API does.
|
|
11
|
-
// This helper avoids repeating the double-cast at every call site.
|
|
12
|
-
function sdkOrderBy(field) {
|
|
13
|
-
return field;
|
|
14
|
-
}
|
|
15
14
|
// Filter-building helpers for Linear SDK queries.
|
|
16
15
|
function eqFilter(value) {
|
|
17
16
|
return { eq: value };
|
|
@@ -22,19 +21,138 @@ function teamIdFilter(teamId) {
|
|
|
22
21
|
function nonEmptyFilter(filter) {
|
|
23
22
|
return Object.keys(filter).length > 0 ? filter : undefined;
|
|
24
23
|
}
|
|
25
|
-
function
|
|
24
|
+
function projectFromCatalog(project) {
|
|
25
|
+
return {
|
|
26
|
+
id: project.id,
|
|
27
|
+
name: project.name,
|
|
28
|
+
description: project.description || undefined,
|
|
29
|
+
state: project.state,
|
|
30
|
+
progress: project.progress,
|
|
31
|
+
teams: project.teams.nodes,
|
|
32
|
+
lead: project.lead ?? undefined,
|
|
33
|
+
targetDate: toISOStringOrUndefined(project.targetDate),
|
|
34
|
+
createdAt: toISOStringOrNow(project.createdAt),
|
|
35
|
+
updatedAt: toISOStringOrNow(project.updatedAt),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function cycleFromCatalog(cycle) {
|
|
39
|
+
return {
|
|
40
|
+
id: cycle.id,
|
|
41
|
+
name: cycle.name ?? undefined,
|
|
42
|
+
number: cycle.number,
|
|
43
|
+
startsAt: toISOStringOrUndefined(cycle.startsAt),
|
|
44
|
+
endsAt: toISOStringOrUndefined(cycle.endsAt),
|
|
45
|
+
isActive: cycle.isActive,
|
|
46
|
+
isPrevious: cycle.isPrevious,
|
|
47
|
+
isNext: cycle.isNext,
|
|
48
|
+
progress: cycle.progress,
|
|
49
|
+
issueCountHistory: cycle.issueCountHistory ?? undefined,
|
|
50
|
+
team: cycle.team ?? undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function issueFromCatalog(issue) {
|
|
54
|
+
return {
|
|
55
|
+
id: issue.id,
|
|
56
|
+
identifier: issue.identifier,
|
|
57
|
+
url: issue.url,
|
|
58
|
+
title: issue.title,
|
|
59
|
+
description: issue.description || undefined,
|
|
60
|
+
priority: issue.priority,
|
|
61
|
+
estimate: issue.estimate || undefined,
|
|
62
|
+
state: issue.state ?? undefined,
|
|
63
|
+
assignee: issue.assignee ?? undefined,
|
|
64
|
+
team: issue.team ?? undefined,
|
|
65
|
+
project: issue.project ?? undefined,
|
|
66
|
+
labels: issue.labels.nodes,
|
|
67
|
+
createdAt: toISOStringOrNow(issue.createdAt),
|
|
68
|
+
updatedAt: toISOStringOrNow(issue.updatedAt),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function errorMessage(error) {
|
|
72
|
+
if (error instanceof Error)
|
|
73
|
+
return error.message;
|
|
74
|
+
if (typeof error === "object" && error !== null) {
|
|
75
|
+
const message = error.message;
|
|
76
|
+
if (typeof message === "string")
|
|
77
|
+
return message;
|
|
78
|
+
}
|
|
79
|
+
return String(error);
|
|
80
|
+
}
|
|
81
|
+
function findRateLimitAdmissionRefusal(error) {
|
|
82
|
+
if (error instanceof RateLimitAdmissionRefusal)
|
|
83
|
+
return error;
|
|
84
|
+
if (typeof error !== "object" || error === null)
|
|
85
|
+
return null;
|
|
86
|
+
const raw = error.raw;
|
|
87
|
+
return raw instanceof RateLimitAdmissionRefusal ? raw : null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Classify every generated SDK operation after the SDK has normalized its raw
|
|
91
|
+
* transport rejection. Patching `client.client.request` is too early: the
|
|
92
|
+
* LinearClient constructor wraps that transport with `parseLinearError`, which
|
|
93
|
+
* would immediately strip our additive classification fields again. Keeping
|
|
94
|
+
* the wrapper here (rather than guessing in `outputError`) also leaves local
|
|
95
|
+
* validation and not-found errors unclassified.
|
|
96
|
+
*/
|
|
97
|
+
export function instrumentLinearGraphQLErrorClassification(client) {
|
|
98
|
+
const sdk = client;
|
|
99
|
+
if (!sdk._request)
|
|
100
|
+
return client;
|
|
101
|
+
const originalRequest = sdk._request.bind(sdk);
|
|
102
|
+
sdk._request = async (document, variables) => {
|
|
103
|
+
try {
|
|
104
|
+
return await originalRequest(document, variables);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const refusal = findRateLimitAdmissionRefusal(error);
|
|
108
|
+
if (refusal)
|
|
109
|
+
throw refusal;
|
|
110
|
+
throw toLinearGraphQLError(error, errorMessage(error));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return client;
|
|
114
|
+
}
|
|
115
|
+
export function instrumentClientCredentialsRenewal(client, renewAccessToken) {
|
|
116
|
+
if (!renewAccessToken)
|
|
117
|
+
return client;
|
|
118
|
+
const transport = client
|
|
119
|
+
.client;
|
|
120
|
+
if (!transport?.request || !transport.setHeader)
|
|
121
|
+
return client;
|
|
122
|
+
const originalRequest = transport.request.bind(transport);
|
|
123
|
+
transport.request = async (document, variables, requestHeaders) => {
|
|
124
|
+
try {
|
|
125
|
+
return await originalRequest(document, variables, requestHeaders);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (graphQLErrorHttpStatus(error) !== 401)
|
|
129
|
+
throw error;
|
|
130
|
+
const token = await renewAccessToken();
|
|
131
|
+
transport.setHeader("authorization", `Bearer ${token}`);
|
|
132
|
+
return originalRequest(document, variables, requestHeaders);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
return client;
|
|
136
|
+
}
|
|
137
|
+
function buildLinearClient(auth, admission, renewAccessToken) {
|
|
26
138
|
if ("oauthToken" in auth) {
|
|
27
|
-
|
|
28
|
-
// which causes the underlying transport to send
|
|
29
|
-
// `Authorization: Bearer <token>` instead of the personal-token shape.
|
|
30
|
-
return new LinearClient({ accessToken: auth.oauthToken });
|
|
139
|
+
return instrumentLinearGraphQLErrorClassification(instrumentClientCredentialsRenewal(instrumentLinearClient(new LinearClient({ accessToken: auth.oauthToken }), auth, { admission }), renewAccessToken));
|
|
31
140
|
}
|
|
32
|
-
return new LinearClient({ apiKey: auth.apiKey })
|
|
141
|
+
return instrumentLinearGraphQLErrorClassification(instrumentLinearClient(new LinearClient({ apiKey: auth.apiKey }), auth, {
|
|
142
|
+
admission,
|
|
143
|
+
}));
|
|
33
144
|
}
|
|
34
145
|
export class LinearService {
|
|
35
146
|
client;
|
|
36
|
-
|
|
37
|
-
|
|
147
|
+
graphQLService;
|
|
148
|
+
constructor(auth, options = {}) {
|
|
149
|
+
this.client = buildLinearClient(auth, options.admission, options.renewAccessToken);
|
|
150
|
+
this.graphQLService =
|
|
151
|
+
options.graphQLService ??
|
|
152
|
+
new GraphQLService(auth, {
|
|
153
|
+
admission: options.admission,
|
|
154
|
+
renewAccessToken: options.renewAccessToken,
|
|
155
|
+
});
|
|
38
156
|
}
|
|
39
157
|
async resolveIssueId(issueId) {
|
|
40
158
|
if (isUuid(issueId)) {
|
|
@@ -149,62 +267,28 @@ export class LinearService {
|
|
|
149
267
|
else if (options.excludeStates && options.excludeStates.length > 0) {
|
|
150
268
|
filter.state = { nin: options.excludeStates };
|
|
151
269
|
}
|
|
152
|
-
// `limit === 0` means "no limit" — paginate the full result set.
|
|
153
|
-
// Otherwise a single page of `limit` projects. DEV-4175: `--all` /
|
|
154
|
-
// `--limit 0` must return every project so callers never make a false
|
|
155
|
-
// "does not exist" determination off a silently truncated page.
|
|
156
270
|
const unlimited = limit === 0;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// `Team.projects` accepts the same name/state ProjectFilter and
|
|
162
|
-
// paginates identically, keeping the filter server-side. (Args stay
|
|
163
|
-
// inline in each branch so `sdkOrderBy`'s generic keeps its contextual
|
|
164
|
-
// type from the SDK signature.)
|
|
165
|
-
let page = options.teamId
|
|
166
|
-
? await (await this.client.team(options.teamId)).projects({
|
|
271
|
+
const projectNodes = [];
|
|
272
|
+
let after = null;
|
|
273
|
+
do {
|
|
274
|
+
const response = await this.graphQLService.rawRequest(GET_PROJECTS_CATALOG_QUERY, {
|
|
167
275
|
filter: nonEmptyFilter(filter),
|
|
276
|
+
teamId: options.teamId ?? "",
|
|
277
|
+
teamScoped: Boolean(options.teamId),
|
|
168
278
|
first: unlimited ? 250 : limit,
|
|
169
|
-
|
|
170
|
-
includeArchived: false,
|
|
171
|
-
})
|
|
172
|
-
: await this.client.projects({
|
|
173
|
-
filter: nonEmptyFilter(filter),
|
|
174
|
-
first: unlimited ? 250 : limit,
|
|
175
|
-
orderBy: sdkOrderBy("updatedAt"),
|
|
176
|
-
includeArchived: false,
|
|
279
|
+
after,
|
|
177
280
|
});
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
281
|
+
const page = options.teamId ? response.team?.projects : response.projects;
|
|
282
|
+
if (!page) {
|
|
283
|
+
throw new Error(options.teamId
|
|
284
|
+
? `Linear project catalog omitted team ${options.teamId}`
|
|
285
|
+
: "Linear project catalog omitted the projects connection");
|
|
183
286
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
]);
|
|
190
|
-
return { project, teams, lead };
|
|
191
|
-
}));
|
|
192
|
-
return projectsWithData.map(({ project, teams, lead }) => ({
|
|
193
|
-
id: project.id,
|
|
194
|
-
name: project.name,
|
|
195
|
-
description: project.description || undefined,
|
|
196
|
-
state: project.state,
|
|
197
|
-
progress: project.progress,
|
|
198
|
-
teams: teams.nodes.map((team) => ({
|
|
199
|
-
id: team.id,
|
|
200
|
-
key: team.key,
|
|
201
|
-
name: team.name,
|
|
202
|
-
})),
|
|
203
|
-
lead: lead ? { id: lead.id, name: lead.name } : undefined,
|
|
204
|
-
targetDate: toISOStringOrUndefined(project.targetDate),
|
|
205
|
-
createdAt: toISOStringOrNow(project.createdAt),
|
|
206
|
-
updatedAt: toISOStringOrNow(project.updatedAt),
|
|
207
|
-
}));
|
|
287
|
+
projectNodes.push(...page.nodes);
|
|
288
|
+
after =
|
|
289
|
+
unlimited && page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
290
|
+
} while (after);
|
|
291
|
+
return projectNodes.map(projectFromCatalog);
|
|
208
292
|
}
|
|
209
293
|
async resolveTeamId(teamKeyOrNameOrId) {
|
|
210
294
|
if (isUuid(teamKeyOrNameOrId)) {
|
|
@@ -277,60 +361,26 @@ export class LinearService {
|
|
|
277
361
|
}
|
|
278
362
|
return statuses.nodes[0].id;
|
|
279
363
|
}
|
|
280
|
-
async buildLabelData(label, scope, team) {
|
|
281
|
-
const parent = await label.parent;
|
|
282
|
-
const labelData = {
|
|
283
|
-
id: label.id,
|
|
284
|
-
name: label.name,
|
|
285
|
-
color: label.color,
|
|
286
|
-
scope,
|
|
287
|
-
team,
|
|
288
|
-
};
|
|
289
|
-
if (parent) {
|
|
290
|
-
const parentLabel = await this.client.issueLabel(parent.id);
|
|
291
|
-
labelData.group = { id: parent.id, name: parentLabel.name };
|
|
292
|
-
}
|
|
293
|
-
return labelData;
|
|
294
|
-
}
|
|
295
364
|
async getLabels(teamFilter, limit = 100, nameFilter) {
|
|
296
|
-
const labels = [];
|
|
297
365
|
const labelFilter = {};
|
|
298
366
|
if (nameFilter) {
|
|
299
367
|
labelFilter.name = { containsIgnoreCase: nameFilter };
|
|
300
368
|
}
|
|
301
369
|
if (teamFilter) {
|
|
302
370
|
const teamId = await this.resolveTeamId(teamFilter);
|
|
303
|
-
const team = await this.client.team(teamId);
|
|
304
371
|
labelFilter.team = teamIdFilter(teamId);
|
|
305
|
-
const teamLabels = await this.client.issueLabels({
|
|
306
|
-
filter: labelFilter,
|
|
307
|
-
first: limit,
|
|
308
|
-
});
|
|
309
|
-
const teamRef = { id: team.id, key: team.key, name: team.name };
|
|
310
|
-
for (const label of teamLabels.nodes) {
|
|
311
|
-
if (label.isGroup) {
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
labels.push(await this.buildLabelData(label, "team", teamRef));
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
const allLabels = await this.client.issueLabels({
|
|
319
|
-
filter: nonEmptyFilter(labelFilter),
|
|
320
|
-
first: limit,
|
|
321
|
-
});
|
|
322
|
-
for (const label of allLabels.nodes) {
|
|
323
|
-
if (label.isGroup) {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
const team = await label.team;
|
|
327
|
-
const scope = team ? "team" : "workspace";
|
|
328
|
-
const teamRef = team
|
|
329
|
-
? { id: team.id, key: team.key, name: team.name }
|
|
330
|
-
: undefined;
|
|
331
|
-
labels.push(await this.buildLabelData(label, scope, teamRef));
|
|
332
|
-
}
|
|
333
372
|
}
|
|
373
|
+
const response = await this.graphQLService.rawRequest(GET_LABELS_CATALOG_QUERY, { filter: nonEmptyFilter(labelFilter), first: limit });
|
|
374
|
+
const labels = response.issueLabels.nodes
|
|
375
|
+
.filter((label) => !label.isGroup)
|
|
376
|
+
.map((label) => ({
|
|
377
|
+
id: label.id,
|
|
378
|
+
name: label.name,
|
|
379
|
+
color: label.color,
|
|
380
|
+
scope: label.team ? "team" : "workspace",
|
|
381
|
+
team: label.team ?? undefined,
|
|
382
|
+
group: label.parent ?? undefined,
|
|
383
|
+
}));
|
|
334
384
|
return { labels };
|
|
335
385
|
}
|
|
336
386
|
async createComment(args) {
|
|
@@ -370,84 +420,20 @@ export class LinearService {
|
|
|
370
420
|
if (activeOnly) {
|
|
371
421
|
filter.isActive = eqFilter(true);
|
|
372
422
|
}
|
|
373
|
-
const
|
|
423
|
+
const response = await this.graphQLService.rawRequest(GET_CYCLES_CATALOG_QUERY, {
|
|
374
424
|
filter: nonEmptyFilter(filter),
|
|
375
|
-
orderBy: sdkOrderBy("createdAt"),
|
|
376
425
|
first: limit ?? DEFAULT_CYCLE_PAGINATION_LIMIT,
|
|
377
426
|
});
|
|
378
|
-
|
|
379
|
-
const team = await cycle.team;
|
|
380
|
-
return {
|
|
381
|
-
id: cycle.id,
|
|
382
|
-
name: cycle.name ?? undefined,
|
|
383
|
-
number: cycle.number,
|
|
384
|
-
startsAt: toISOStringOrUndefined(cycle.startsAt),
|
|
385
|
-
endsAt: toISOStringOrUndefined(cycle.endsAt),
|
|
386
|
-
isActive: cycle.isActive,
|
|
387
|
-
isPrevious: cycle.isPrevious,
|
|
388
|
-
isNext: cycle.isNext,
|
|
389
|
-
progress: cycle.progress,
|
|
390
|
-
issueCountHistory: cycle.issueCountHistory,
|
|
391
|
-
team: team
|
|
392
|
-
? { id: team.id, key: team.key, name: team.name }
|
|
393
|
-
: undefined,
|
|
394
|
-
};
|
|
395
|
-
}));
|
|
396
|
-
return cyclesWithData;
|
|
427
|
+
return response.cycles.nodes.map(cycleFromCatalog);
|
|
397
428
|
}
|
|
398
429
|
async getCycleById(cycleId, issuesLimit = 50) {
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
]);
|
|
404
|
-
const issues = [];
|
|
405
|
-
for (const issue of issuesConnection.nodes) {
|
|
406
|
-
const [state, assignee, issueTeam, project, labels] = await Promise.all([
|
|
407
|
-
issue.state,
|
|
408
|
-
issue.assignee,
|
|
409
|
-
issue.team,
|
|
410
|
-
issue.project,
|
|
411
|
-
issue.labels(),
|
|
412
|
-
]);
|
|
413
|
-
issues.push({
|
|
414
|
-
id: issue.id,
|
|
415
|
-
identifier: issue.identifier,
|
|
416
|
-
url: issue.url,
|
|
417
|
-
title: issue.title,
|
|
418
|
-
description: issue.description || undefined,
|
|
419
|
-
// Linear SDK types `priority` as `number`; the GraphQL schema only
|
|
420
|
-
// emits 0-4, so the cast is safe and the runtime range is
|
|
421
|
-
// guaranteed by Linear's server-side schema.
|
|
422
|
-
priority: issue.priority,
|
|
423
|
-
estimate: issue.estimate || undefined,
|
|
424
|
-
state: state ? { id: state.id, name: state.name } : undefined,
|
|
425
|
-
assignee: assignee
|
|
426
|
-
? { id: assignee.id, name: assignee.name }
|
|
427
|
-
: undefined,
|
|
428
|
-
team: issueTeam
|
|
429
|
-
? { id: issueTeam.id, key: issueTeam.key, name: issueTeam.name }
|
|
430
|
-
: undefined,
|
|
431
|
-
project: project ? { id: project.id, name: project.name } : undefined,
|
|
432
|
-
labels: labels.nodes.map((label) => ({
|
|
433
|
-
id: label.id,
|
|
434
|
-
name: label.name,
|
|
435
|
-
})),
|
|
436
|
-
createdAt: toISOStringOrNow(issue.createdAt),
|
|
437
|
-
updatedAt: toISOStringOrNow(issue.updatedAt),
|
|
438
|
-
});
|
|
439
|
-
}
|
|
430
|
+
const response = await this.graphQLService.rawRequest(GET_CYCLE_DETAIL_QUERY, { id: cycleId, issuesFirst: issuesLimit });
|
|
431
|
+
const cycle = response.cycle;
|
|
432
|
+
if (!cycle)
|
|
433
|
+
throw notFoundError("Cycle", cycleId);
|
|
440
434
|
return {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
number: cycle.number,
|
|
444
|
-
startsAt: toISOStringOrUndefined(cycle.startsAt),
|
|
445
|
-
endsAt: toISOStringOrUndefined(cycle.endsAt),
|
|
446
|
-
isActive: cycle.isActive,
|
|
447
|
-
progress: cycle.progress,
|
|
448
|
-
issueCountHistory: cycle.issueCountHistory,
|
|
449
|
-
team: team ? { id: team.id, key: team.key, name: team.name } : undefined,
|
|
450
|
-
issues,
|
|
435
|
+
...cycleFromCatalog(cycle),
|
|
436
|
+
issues: cycle.issues.nodes.map(issueFromCatalog),
|
|
451
437
|
};
|
|
452
438
|
}
|
|
453
439
|
async resolveCycleId(cycleNameOrId, teamFilter) {
|
|
@@ -461,14 +447,10 @@ export class LinearService {
|
|
|
461
447
|
const teamId = await this.resolveTeamId(teamFilter);
|
|
462
448
|
filter.team = teamIdFilter(teamId);
|
|
463
449
|
}
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
first: 10,
|
|
467
|
-
});
|
|
468
|
-
const cyclesData = cyclesConnection.nodes;
|
|
450
|
+
const response = await this.graphQLService.rawRequest(GET_CYCLES_CATALOG_QUERY, { filter, first: 10 });
|
|
451
|
+
const cyclesData = response.cycles.nodes;
|
|
469
452
|
const nodes = [];
|
|
470
453
|
for (const cycle of cyclesData) {
|
|
471
|
-
const team = await cycle.team;
|
|
472
454
|
nodes.push({
|
|
473
455
|
id: cycle.id,
|
|
474
456
|
name: cycle.name ?? undefined,
|
|
@@ -477,9 +459,7 @@ export class LinearService {
|
|
|
477
459
|
isActive: cycle.isActive,
|
|
478
460
|
isNext: cycle.isNext,
|
|
479
461
|
isPrevious: cycle.isPrevious,
|
|
480
|
-
team: team
|
|
481
|
-
? { id: team.id, key: team.key, name: team.name }
|
|
482
|
-
: undefined,
|
|
462
|
+
team: cycle.team ?? undefined,
|
|
483
463
|
});
|
|
484
464
|
}
|
|
485
465
|
if (nodes.length === 0) {
|
|
@@ -531,13 +511,14 @@ export class LinearService {
|
|
|
531
511
|
// worse than resolving to an archived project, which the caller can
|
|
532
512
|
// inspect via the returned UUID. Name resolution stays active-only
|
|
533
513
|
// because names collide more readily than slug-ids do.
|
|
534
|
-
const bySlug = await this.
|
|
514
|
+
const bySlug = await this.graphQLService.rawRequest(RESOLVE_PROJECT_QUERY, {
|
|
535
515
|
filter: { slugId: { eq: slugId } },
|
|
536
|
-
|
|
516
|
+
teamId: "",
|
|
517
|
+
teamScoped: false,
|
|
537
518
|
includeArchived: true,
|
|
538
519
|
});
|
|
539
|
-
if (bySlug.nodes.length
|
|
540
|
-
return bySlug.nodes[0].id;
|
|
520
|
+
if (bySlug.projects?.nodes.length) {
|
|
521
|
+
return bySlug.projects.nodes[0].id;
|
|
541
522
|
}
|
|
542
523
|
// Slug-id form was syntactically valid but didn't match a project
|
|
543
524
|
// — fall through to name resolution would be misleading (the user
|
|
@@ -558,28 +539,23 @@ export class LinearService {
|
|
|
558
539
|
// effective project-name collisions at a handful in practice; if a
|
|
559
540
|
// workspace ever exceeds this we'll see it as a still-ambiguous error
|
|
560
541
|
// listing the first 5 teams — better than a silent wrong pick.
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
:
|
|
564
|
-
|
|
542
|
+
const response = await this.graphQLService.rawRequest(RESOLVE_PROJECT_QUERY, {
|
|
543
|
+
filter,
|
|
544
|
+
teamId: teamId ?? "",
|
|
545
|
+
teamScoped: Boolean(teamId),
|
|
546
|
+
includeArchived: false,
|
|
547
|
+
});
|
|
548
|
+
const projects = teamId
|
|
549
|
+
? response.team?.projects.nodes
|
|
550
|
+
: response.projects?.nodes;
|
|
551
|
+
if (!projects?.length) {
|
|
565
552
|
const context = teamInput ? `on team "${teamInput}"` : undefined;
|
|
566
553
|
throw notFoundError("Project", projectInput, context);
|
|
567
554
|
}
|
|
568
|
-
if (
|
|
569
|
-
return
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// names them concretely. Bounded N ≤ 5 keeps the per-edge resolver
|
|
573
|
-
// promise cheap (handful of round-trips on the rare ambiguous path).
|
|
574
|
-
const candidates = await Promise.all(projectsConnection.nodes.map(async (p) => {
|
|
575
|
-
const teams = await p.teams();
|
|
576
|
-
return {
|
|
577
|
-
id: p.id,
|
|
578
|
-
name: p.name,
|
|
579
|
-
teamKeys: teams.nodes.map((t) => t.key),
|
|
580
|
-
};
|
|
581
|
-
}));
|
|
582
|
-
const descriptions = candidates.map((c) => `${c.id} (teams: ${c.teamKeys.join(", ") || "none"})`);
|
|
555
|
+
if (projects.length === 1) {
|
|
556
|
+
return projects[0].id;
|
|
557
|
+
}
|
|
558
|
+
const descriptions = projects.map((project) => `${project.id} (teams: ${project.teams.nodes.map((team) => team.key).join(", ") || "none"})`);
|
|
583
559
|
throw multipleMatchesError("project", projectInput, descriptions, "scope with --team, or pass the project URL/slug-id");
|
|
584
560
|
}
|
|
585
561
|
/**
|
|
@@ -634,7 +610,11 @@ export class LinearService {
|
|
|
634
610
|
export async function createLinearService(options) {
|
|
635
611
|
const auth = await getActiveAuth(options);
|
|
636
612
|
if (auth.kind === "oauth") {
|
|
637
|
-
|
|
613
|
+
const credential = { oauthToken: auth.token };
|
|
614
|
+
return new LinearService(credential, {
|
|
615
|
+
admission: createOAuthProfileRateLimitAdmission(credential, auth.oauth.clientId, auth.oauth.viewerId),
|
|
616
|
+
renewAccessToken: createClientCredentialsTokenRenewal(auth.oauth),
|
|
617
|
+
});
|
|
638
618
|
}
|
|
639
619
|
return new LinearService({ apiKey: auth.token });
|
|
640
620
|
}
|
package/dist/utils/output.d.ts
CHANGED
|
@@ -1,4 +1,43 @@
|
|
|
1
1
|
type OutputFormat = "json" | "summary";
|
|
2
|
+
export interface RateLimitInfo {
|
|
3
|
+
/** Maximum requests allowed in the current Linear API window. */
|
|
4
|
+
limit?: number;
|
|
5
|
+
/** Requests still available in the current Linear API window. */
|
|
6
|
+
remaining?: number;
|
|
7
|
+
/** UTC instant when the current Linear API window resets. */
|
|
8
|
+
resetAt?: string;
|
|
9
|
+
/** Number of GraphQL responses observed during this command. */
|
|
10
|
+
observedRequests?: number;
|
|
11
|
+
/** Lowest global request headroom observed during this command. */
|
|
12
|
+
minimumRemaining?: number;
|
|
13
|
+
/** Complexity consumed and remaining in Linear's complexity bucket. */
|
|
14
|
+
complexity?: {
|
|
15
|
+
/** Complexity of the most recently observed response. */
|
|
16
|
+
cost?: number;
|
|
17
|
+
/** Sum of response complexity observed during this command. */
|
|
18
|
+
totalCost?: number;
|
|
19
|
+
limit?: number;
|
|
20
|
+
remaining?: number;
|
|
21
|
+
minimumRemaining?: number;
|
|
22
|
+
resetAt?: string;
|
|
23
|
+
};
|
|
24
|
+
/** Per-endpoint request buckets observed during this command. */
|
|
25
|
+
endpoints?: Record<string, {
|
|
26
|
+
limit?: number;
|
|
27
|
+
remaining?: number;
|
|
28
|
+
minimumRemaining?: number;
|
|
29
|
+
resetAt?: string;
|
|
30
|
+
observedRequests?: number;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Record a quota observation for the command's eventual output.
|
|
35
|
+
*
|
|
36
|
+
* The latest response remains authoritative for current remaining/reset
|
|
37
|
+
* values. Multi-request commands additionally retain their total observed
|
|
38
|
+
* request/complexity cost and the lowest headroom seen in every bucket.
|
|
39
|
+
*/
|
|
40
|
+
export declare function recordRateLimitInfo(info: RateLimitInfo): void;
|
|
2
41
|
export declare function setRawMode(enabled: boolean): void;
|
|
3
42
|
/**
|
|
4
43
|
* `--quiet` (write commands only): collapse the success payload to a single
|