@enrichlayer/el-linear 1.2.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/LICENSE +21 -0
- package/README.md +219 -0
- package/claude-skills/linear-operations/SKILL.md +315 -0
- package/claude-skills/linear-operations/evals/evals.json +46 -0
- package/dist/commands/attachments.d.ts +2 -0
- package/dist/commands/attachments.js +57 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +309 -0
- package/dist/commands/comments.d.ts +2 -0
- package/dist/commands/comments.js +272 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +15 -0
- package/dist/commands/cycles.d.ts +2 -0
- package/dist/commands/cycles.js +63 -0
- package/dist/commands/documents.d.ts +2 -0
- package/dist/commands/documents.js +175 -0
- package/dist/commands/embeds.d.ts +2 -0
- package/dist/commands/embeds.js +65 -0
- package/dist/commands/gdoc.d.ts +2 -0
- package/dist/commands/gdoc.js +37 -0
- package/dist/commands/graphql.d.ts +2 -0
- package/dist/commands/graphql.js +70 -0
- package/dist/commands/init/aliases.d.ts +109 -0
- package/dist/commands/init/aliases.js +569 -0
- package/dist/commands/init/defaults.d.ts +25 -0
- package/dist/commands/init/defaults.js +112 -0
- package/dist/commands/init/index.d.ts +18 -0
- package/dist/commands/init/index.js +182 -0
- package/dist/commands/init/shared.d.ts +88 -0
- package/dist/commands/init/shared.js +164 -0
- package/dist/commands/init/token.d.ts +50 -0
- package/dist/commands/init/token.js +141 -0
- package/dist/commands/init/workspace.d.ts +20 -0
- package/dist/commands/init/workspace.js +80 -0
- package/dist/commands/issue-id.d.ts +28 -0
- package/dist/commands/issue-id.js +81 -0
- package/dist/commands/issues.d.ts +2 -0
- package/dist/commands/issues.js +1145 -0
- package/dist/commands/labels.d.ts +2 -0
- package/dist/commands/labels.js +100 -0
- package/dist/commands/project-milestones.d.ts +2 -0
- package/dist/commands/project-milestones.js +143 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +336 -0
- package/dist/commands/read-shortcut.d.ts +6 -0
- package/dist/commands/read-shortcut.js +69 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +142 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +171 -0
- package/dist/commands/teams.d.ts +2 -0
- package/dist/commands/teams.js +19 -0
- package/dist/commands/templates.d.ts +2 -0
- package/dist/commands/templates.js +58 -0
- package/dist/commands/users.d.ts +2 -0
- package/dist/commands/users.js +17 -0
- package/dist/config/config.d.ts +43 -0
- package/dist/config/config.js +81 -0
- package/dist/config/issue-validation.d.ts +39 -0
- package/dist/config/issue-validation.js +264 -0
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +22 -0
- package/dist/config/resolver.d.ts +25 -0
- package/dist/config/resolver.js +183 -0
- package/dist/config/status-defaults.d.ts +13 -0
- package/dist/config/status-defaults.js +20 -0
- package/dist/config/term-enforcer.d.ts +31 -0
- package/dist/config/term-enforcer.js +69 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +76 -0
- package/dist/queries/attachments.d.ts +3 -0
- package/dist/queries/attachments.js +35 -0
- package/dist/queries/comments.d.ts +3 -0
- package/dist/queries/comments.js +64 -0
- package/dist/queries/common.d.ts +2 -0
- package/dist/queries/common.js +109 -0
- package/dist/queries/cycles.d.ts +2 -0
- package/dist/queries/cycles.js +40 -0
- package/dist/queries/documents.d.ts +5 -0
- package/dist/queries/documents.js +67 -0
- package/dist/queries/introspect.d.ts +2 -0
- package/dist/queries/introspect.js +24 -0
- package/dist/queries/issues.d.ts +23 -0
- package/dist/queries/issues.js +380 -0
- package/dist/queries/labels.d.ts +4 -0
- package/dist/queries/labels.js +53 -0
- package/dist/queries/project-milestones.d.ts +6 -0
- package/dist/queries/project-milestones.js +125 -0
- package/dist/queries/projects.d.ts +7 -0
- package/dist/queries/projects.js +104 -0
- package/dist/queries/releases.d.ts +4 -0
- package/dist/queries/releases.js +85 -0
- package/dist/queries/search.d.ts +1 -0
- package/dist/queries/search.js +35 -0
- package/dist/queries/templates.d.ts +2 -0
- package/dist/queries/templates.js +30 -0
- package/dist/types/linear.d.ts +217 -0
- package/dist/types/linear.js +6 -0
- package/dist/utils/auth.d.ts +4 -0
- package/dist/utils/auth.js +23 -0
- package/dist/utils/auto-link-references.d.ts +47 -0
- package/dist/utils/auto-link-references.js +188 -0
- package/dist/utils/date-format.d.ts +4 -0
- package/dist/utils/date-format.js +8 -0
- package/dist/utils/download-uploads.d.ts +7 -0
- package/dist/utils/download-uploads.js +88 -0
- package/dist/utils/embed-parser.d.ts +8 -0
- package/dist/utils/embed-parser.js +54 -0
- package/dist/utils/error-messages.d.ts +4 -0
- package/dist/utils/error-messages.js +17 -0
- package/dist/utils/file-service.d.ts +13 -0
- package/dist/utils/file-service.js +239 -0
- package/dist/utils/gdoc-parser.d.ts +45 -0
- package/dist/utils/gdoc-parser.js +107 -0
- package/dist/utils/graphql-attachments-service.d.ts +12 -0
- package/dist/utils/graphql-attachments-service.js +46 -0
- package/dist/utils/graphql-documents-service.d.ts +18 -0
- package/dist/utils/graphql-documents-service.js +97 -0
- package/dist/utils/graphql-issues-service.d.ts +49 -0
- package/dist/utils/graphql-issues-service.js +925 -0
- package/dist/utils/graphql-service.d.ts +8 -0
- package/dist/utils/graphql-service.js +35 -0
- package/dist/utils/identifier-parser.d.ts +7 -0
- package/dist/utils/identifier-parser.js +20 -0
- package/dist/utils/issue-reference-extractor.d.ts +18 -0
- package/dist/utils/issue-reference-extractor.js +95 -0
- package/dist/utils/issue-reference-wrapper.d.ts +12 -0
- package/dist/utils/issue-reference-wrapper.js +91 -0
- package/dist/utils/linear-service.d.ts +26 -0
- package/dist/utils/linear-service.js +442 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +8 -0
- package/dist/utils/markdown-prosemirror.d.ts +24 -0
- package/dist/utils/markdown-prosemirror.js +325 -0
- package/dist/utils/mention-resolver.d.ts +31 -0
- package/dist/utils/mention-resolver.js +234 -0
- package/dist/utils/output.d.ts +7 -0
- package/dist/utils/output.js +125 -0
- package/dist/utils/table-formatter.d.ts +4 -0
- package/dist/utils/table-formatter.js +249 -0
- package/dist/utils/usage.d.ts +2 -0
- package/dist/utils/usage.js +24 -0
- package/dist/utils/uuid.d.ts +2 -0
- package/dist/utils/uuid.js +8 -0
- package/dist/utils/validate-references.d.ts +10 -0
- package/dist/utils/validate-references.js +33 -0
- package/dist/utils/validators.d.ts +7 -0
- package/dist/utils/validators.js +71 -0
- package/dist/utils/workspace-url.d.ts +4 -0
- package/dist/utils/workspace-url.js +44 -0
- package/package.json +71 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { LinearClient } from "@linear/sdk";
|
|
2
|
+
import { resolveUserDisplayName } from "../config/resolver.js";
|
|
3
|
+
import { getApiToken } from "./auth.js";
|
|
4
|
+
import { toISOStringOrNow, toISOStringOrUndefined } from "./date-format.js";
|
|
5
|
+
import { multipleMatchesError, notFoundError } from "./error-messages.js";
|
|
6
|
+
import { parseIssueIdentifier } from "./identifier-parser.js";
|
|
7
|
+
import { isUuid } from "./uuid.js";
|
|
8
|
+
const DEFAULT_CYCLE_PAGINATION_LIMIT = 250;
|
|
9
|
+
// The Linear SDK types don't accept string orderBy values, but the API does.
|
|
10
|
+
// This helper avoids repeating the double-cast at every call site.
|
|
11
|
+
function sdkOrderBy(field) {
|
|
12
|
+
return field;
|
|
13
|
+
}
|
|
14
|
+
// Filter-building helpers for Linear SDK queries.
|
|
15
|
+
function eqFilter(value) {
|
|
16
|
+
return { eq: value };
|
|
17
|
+
}
|
|
18
|
+
function teamIdFilter(teamId) {
|
|
19
|
+
return { id: { eq: teamId } };
|
|
20
|
+
}
|
|
21
|
+
function nonEmptyFilter(filter) {
|
|
22
|
+
return Object.keys(filter).length > 0 ? filter : undefined;
|
|
23
|
+
}
|
|
24
|
+
export class LinearService {
|
|
25
|
+
client;
|
|
26
|
+
constructor(apiToken) {
|
|
27
|
+
this.client = new LinearClient({ apiKey: apiToken });
|
|
28
|
+
}
|
|
29
|
+
async resolveIssueId(issueId) {
|
|
30
|
+
if (isUuid(issueId)) {
|
|
31
|
+
return issueId;
|
|
32
|
+
}
|
|
33
|
+
const { teamKey, issueNumber } = parseIssueIdentifier(issueId);
|
|
34
|
+
const issues = await this.client.issues({
|
|
35
|
+
filter: {
|
|
36
|
+
number: { eq: issueNumber },
|
|
37
|
+
team: { key: { eq: teamKey } },
|
|
38
|
+
},
|
|
39
|
+
first: 1,
|
|
40
|
+
});
|
|
41
|
+
if (issues.nodes.length === 0) {
|
|
42
|
+
throw notFoundError("Issue", issueId);
|
|
43
|
+
}
|
|
44
|
+
return issues.nodes[0].id;
|
|
45
|
+
}
|
|
46
|
+
async getTeams(limit = 100) {
|
|
47
|
+
const teamsConnection = await this.client.teams({ first: limit });
|
|
48
|
+
const teams = teamsConnection.nodes.map((team) => ({
|
|
49
|
+
id: team.id,
|
|
50
|
+
key: team.key,
|
|
51
|
+
name: team.name,
|
|
52
|
+
description: team.description || null,
|
|
53
|
+
}));
|
|
54
|
+
return teams.sort((a, b) => a.name.localeCompare(b.name));
|
|
55
|
+
}
|
|
56
|
+
async resolveUserId(nameOrEmailOrId) {
|
|
57
|
+
if (isUuid(nameOrEmailOrId)) {
|
|
58
|
+
return nameOrEmailOrId;
|
|
59
|
+
}
|
|
60
|
+
if (nameOrEmailOrId.includes("@")) {
|
|
61
|
+
const byEmail = await this.client.users({
|
|
62
|
+
filter: { email: { eq: nameOrEmailOrId } },
|
|
63
|
+
first: 1,
|
|
64
|
+
});
|
|
65
|
+
if (byEmail.nodes.length > 0) {
|
|
66
|
+
return byEmail.nodes[0].id;
|
|
67
|
+
}
|
|
68
|
+
throw notFoundError("User", nameOrEmailOrId);
|
|
69
|
+
}
|
|
70
|
+
const byDisplay = await this.client.users({
|
|
71
|
+
filter: { displayName: { eqIgnoreCase: nameOrEmailOrId } },
|
|
72
|
+
first: 5,
|
|
73
|
+
});
|
|
74
|
+
if (byDisplay.nodes.length === 1) {
|
|
75
|
+
return byDisplay.nodes[0].id;
|
|
76
|
+
}
|
|
77
|
+
if (byDisplay.nodes.length > 1) {
|
|
78
|
+
throw multipleMatchesError("User", nameOrEmailOrId, byDisplay.nodes.map((u) => `${u.displayName} (${u.email})`), "use the full email or UUID instead");
|
|
79
|
+
}
|
|
80
|
+
const byName = await this.client.users({
|
|
81
|
+
filter: { name: { containsIgnoreCase: nameOrEmailOrId } },
|
|
82
|
+
first: 5,
|
|
83
|
+
});
|
|
84
|
+
if (byName.nodes.length === 1) {
|
|
85
|
+
return byName.nodes[0].id;
|
|
86
|
+
}
|
|
87
|
+
if (byName.nodes.length > 1) {
|
|
88
|
+
throw multipleMatchesError("User", nameOrEmailOrId, byName.nodes.map((u) => `${u.name} (${u.email})`), "use the full email or UUID instead");
|
|
89
|
+
}
|
|
90
|
+
throw notFoundError("User", nameOrEmailOrId);
|
|
91
|
+
}
|
|
92
|
+
async getUsers(activeOnly, limit = 100) {
|
|
93
|
+
const filter = {};
|
|
94
|
+
if (activeOnly) {
|
|
95
|
+
filter.active = eqFilter(true);
|
|
96
|
+
}
|
|
97
|
+
const usersConnection = await this.client.users({
|
|
98
|
+
filter: nonEmptyFilter(filter),
|
|
99
|
+
first: limit,
|
|
100
|
+
});
|
|
101
|
+
const users = usersConnection.nodes.map((user) => ({
|
|
102
|
+
id: user.id,
|
|
103
|
+
name: user.name,
|
|
104
|
+
displayName: user.displayName,
|
|
105
|
+
email: user.email,
|
|
106
|
+
active: user.active,
|
|
107
|
+
}));
|
|
108
|
+
return users.sort((a, b) => a.name.localeCompare(b.name));
|
|
109
|
+
}
|
|
110
|
+
async getProjects(limit = 100) {
|
|
111
|
+
const projects = await this.client.projects({
|
|
112
|
+
first: limit,
|
|
113
|
+
orderBy: sdkOrderBy("updatedAt"),
|
|
114
|
+
includeArchived: false,
|
|
115
|
+
});
|
|
116
|
+
const projectsWithData = await Promise.all(projects.nodes.map(async (project) => {
|
|
117
|
+
const [teams, lead] = await Promise.all([
|
|
118
|
+
project.teams(),
|
|
119
|
+
project.lead,
|
|
120
|
+
]);
|
|
121
|
+
return { project, teams, lead };
|
|
122
|
+
}));
|
|
123
|
+
return projectsWithData.map(({ project, teams, lead }) => ({
|
|
124
|
+
id: project.id,
|
|
125
|
+
name: project.name,
|
|
126
|
+
description: project.description || undefined,
|
|
127
|
+
state: project.state,
|
|
128
|
+
progress: project.progress,
|
|
129
|
+
teams: teams.nodes.map((team) => ({
|
|
130
|
+
id: team.id,
|
|
131
|
+
key: team.key,
|
|
132
|
+
name: team.name,
|
|
133
|
+
})),
|
|
134
|
+
lead: lead ? { id: lead.id, name: lead.name } : undefined,
|
|
135
|
+
targetDate: toISOStringOrUndefined(project.targetDate),
|
|
136
|
+
createdAt: toISOStringOrNow(project.createdAt),
|
|
137
|
+
updatedAt: toISOStringOrNow(project.updatedAt),
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
async resolveTeamId(teamKeyOrNameOrId) {
|
|
141
|
+
if (isUuid(teamKeyOrNameOrId)) {
|
|
142
|
+
return teamKeyOrNameOrId;
|
|
143
|
+
}
|
|
144
|
+
// 1. Exact match via server-side filter (case-sensitive key, case-insensitive name)
|
|
145
|
+
const byKey = await this.client.teams({
|
|
146
|
+
filter: { key: { eq: teamKeyOrNameOrId.toUpperCase() } },
|
|
147
|
+
first: 1,
|
|
148
|
+
});
|
|
149
|
+
if (byKey.nodes.length > 0) {
|
|
150
|
+
return byKey.nodes[0].id;
|
|
151
|
+
}
|
|
152
|
+
const byName = await this.client.teams({
|
|
153
|
+
filter: { name: { eqIgnoreCase: teamKeyOrNameOrId } },
|
|
154
|
+
first: 1,
|
|
155
|
+
});
|
|
156
|
+
if (byName.nodes.length > 0) {
|
|
157
|
+
return byName.nodes[0].id;
|
|
158
|
+
}
|
|
159
|
+
// 2. No exact match — fetch all teams for prefix matching
|
|
160
|
+
let page = await this.client.teams({ first: 50 });
|
|
161
|
+
const allNodes = [...page.nodes];
|
|
162
|
+
while (page.pageInfo.hasNextPage) {
|
|
163
|
+
page = await page.fetchNext();
|
|
164
|
+
allNodes.push(...page.nodes);
|
|
165
|
+
}
|
|
166
|
+
const input = teamKeyOrNameOrId.toLowerCase();
|
|
167
|
+
const prefixMatches = allNodes.filter((team) => team.key.toLowerCase().startsWith(input) ||
|
|
168
|
+
team.name.toLowerCase().startsWith(input));
|
|
169
|
+
if (prefixMatches.length === 1) {
|
|
170
|
+
return prefixMatches[0].id;
|
|
171
|
+
}
|
|
172
|
+
if (prefixMatches.length > 1) {
|
|
173
|
+
throw multipleMatchesError("team", teamKeyOrNameOrId, prefixMatches.map((t) => `${t.key} (${t.name})`), "use a more specific prefix or the full team key");
|
|
174
|
+
}
|
|
175
|
+
// 3. No match
|
|
176
|
+
const available = allNodes.map((t) => `${t.key} (${t.name})`);
|
|
177
|
+
throw notFoundError("Team", teamKeyOrNameOrId, undefined, `\n Available teams: ${available.join(", ")}`);
|
|
178
|
+
}
|
|
179
|
+
async resolveStatusId(statusName, teamId) {
|
|
180
|
+
if (isUuid(statusName)) {
|
|
181
|
+
return statusName;
|
|
182
|
+
}
|
|
183
|
+
const filter = {
|
|
184
|
+
name: { eqIgnoreCase: statusName },
|
|
185
|
+
};
|
|
186
|
+
if (teamId) {
|
|
187
|
+
filter.team = teamIdFilter(teamId);
|
|
188
|
+
}
|
|
189
|
+
const statuses = await this.client.workflowStates({
|
|
190
|
+
filter,
|
|
191
|
+
first: 1,
|
|
192
|
+
});
|
|
193
|
+
if (statuses.nodes.length === 0) {
|
|
194
|
+
// Fallback: if "Triage" not found, try "Backlog" as a safe default
|
|
195
|
+
if (statusName.toLowerCase() === "triage" && teamId) {
|
|
196
|
+
const fallback = await this.client.workflowStates({
|
|
197
|
+
filter: {
|
|
198
|
+
name: { eqIgnoreCase: "Backlog" },
|
|
199
|
+
team: teamIdFilter(teamId),
|
|
200
|
+
},
|
|
201
|
+
first: 1,
|
|
202
|
+
});
|
|
203
|
+
if (fallback.nodes.length > 0) {
|
|
204
|
+
return fallback.nodes[0].id;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
throw notFoundError("Status", statusName, teamId ? `for team ${teamId}` : undefined);
|
|
208
|
+
}
|
|
209
|
+
return statuses.nodes[0].id;
|
|
210
|
+
}
|
|
211
|
+
async buildLabelData(label, scope, team) {
|
|
212
|
+
const parent = await label.parent;
|
|
213
|
+
const labelData = {
|
|
214
|
+
id: label.id,
|
|
215
|
+
name: label.name,
|
|
216
|
+
color: label.color,
|
|
217
|
+
scope,
|
|
218
|
+
team,
|
|
219
|
+
};
|
|
220
|
+
if (parent) {
|
|
221
|
+
const parentLabel = await this.client.issueLabel(parent.id);
|
|
222
|
+
labelData.group = { id: parent.id, name: parentLabel.name };
|
|
223
|
+
}
|
|
224
|
+
return labelData;
|
|
225
|
+
}
|
|
226
|
+
async getLabels(teamFilter, limit = 100) {
|
|
227
|
+
const labels = [];
|
|
228
|
+
if (teamFilter) {
|
|
229
|
+
const teamId = await this.resolveTeamId(teamFilter);
|
|
230
|
+
const team = await this.client.team(teamId);
|
|
231
|
+
const teamLabels = await this.client.issueLabels({
|
|
232
|
+
filter: { team: teamIdFilter(teamId) },
|
|
233
|
+
first: limit,
|
|
234
|
+
});
|
|
235
|
+
const teamRef = { id: team.id, key: team.key, name: team.name };
|
|
236
|
+
for (const label of teamLabels.nodes) {
|
|
237
|
+
if (label.isGroup) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
labels.push(await this.buildLabelData(label, "team", teamRef));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const allLabels = await this.client.issueLabels({ first: limit });
|
|
245
|
+
for (const label of allLabels.nodes) {
|
|
246
|
+
if (label.isGroup) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const team = await label.team;
|
|
250
|
+
const scope = team ? "team" : "workspace";
|
|
251
|
+
const teamRef = team
|
|
252
|
+
? { id: team.id, key: team.key, name: team.name }
|
|
253
|
+
: undefined;
|
|
254
|
+
labels.push(await this.buildLabelData(label, scope, teamRef));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { labels };
|
|
258
|
+
}
|
|
259
|
+
async createComment(args) {
|
|
260
|
+
const payload = await this.client.createComment({
|
|
261
|
+
issueId: args.issueId,
|
|
262
|
+
body: args.body,
|
|
263
|
+
});
|
|
264
|
+
if (!payload.success) {
|
|
265
|
+
throw new Error("Failed to create comment");
|
|
266
|
+
}
|
|
267
|
+
const comment = await payload.comment;
|
|
268
|
+
if (!comment) {
|
|
269
|
+
throw new Error("Failed to retrieve created comment");
|
|
270
|
+
}
|
|
271
|
+
const user = await comment.user;
|
|
272
|
+
if (!user) {
|
|
273
|
+
throw new Error("Failed to retrieve comment user information");
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
id: comment.id,
|
|
277
|
+
body: comment.body,
|
|
278
|
+
user: {
|
|
279
|
+
id: user.id,
|
|
280
|
+
name: resolveUserDisplayName(user.id, user.name),
|
|
281
|
+
url: user.url || undefined,
|
|
282
|
+
},
|
|
283
|
+
createdAt: comment.createdAt.toISOString(),
|
|
284
|
+
updatedAt: comment.updatedAt.toISOString(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async getCycles(teamFilter, activeOnly, limit) {
|
|
288
|
+
const filter = {};
|
|
289
|
+
if (teamFilter) {
|
|
290
|
+
const teamId = await this.resolveTeamId(teamFilter);
|
|
291
|
+
filter.team = teamIdFilter(teamId);
|
|
292
|
+
}
|
|
293
|
+
if (activeOnly) {
|
|
294
|
+
filter.isActive = eqFilter(true);
|
|
295
|
+
}
|
|
296
|
+
const cyclesConnection = await this.client.cycles({
|
|
297
|
+
filter: nonEmptyFilter(filter),
|
|
298
|
+
orderBy: sdkOrderBy("createdAt"),
|
|
299
|
+
first: limit ?? DEFAULT_CYCLE_PAGINATION_LIMIT,
|
|
300
|
+
});
|
|
301
|
+
const cyclesWithData = await Promise.all(cyclesConnection.nodes.map(async (cycle) => {
|
|
302
|
+
const team = await cycle.team;
|
|
303
|
+
return {
|
|
304
|
+
id: cycle.id,
|
|
305
|
+
name: cycle.name ?? undefined,
|
|
306
|
+
number: cycle.number,
|
|
307
|
+
startsAt: toISOStringOrUndefined(cycle.startsAt),
|
|
308
|
+
endsAt: toISOStringOrUndefined(cycle.endsAt),
|
|
309
|
+
isActive: cycle.isActive,
|
|
310
|
+
isPrevious: cycle.isPrevious,
|
|
311
|
+
isNext: cycle.isNext,
|
|
312
|
+
progress: cycle.progress,
|
|
313
|
+
issueCountHistory: cycle.issueCountHistory,
|
|
314
|
+
team: team
|
|
315
|
+
? { id: team.id, key: team.key, name: team.name }
|
|
316
|
+
: undefined,
|
|
317
|
+
};
|
|
318
|
+
}));
|
|
319
|
+
return cyclesWithData;
|
|
320
|
+
}
|
|
321
|
+
async getCycleById(cycleId, issuesLimit = 50) {
|
|
322
|
+
const cycle = await this.client.cycle(cycleId);
|
|
323
|
+
const [team, issuesConnection] = await Promise.all([
|
|
324
|
+
cycle.team,
|
|
325
|
+
cycle.issues({ first: issuesLimit }),
|
|
326
|
+
]);
|
|
327
|
+
const issues = [];
|
|
328
|
+
for (const issue of issuesConnection.nodes) {
|
|
329
|
+
const [state, assignee, issueTeam, project, labels] = await Promise.all([
|
|
330
|
+
issue.state,
|
|
331
|
+
issue.assignee,
|
|
332
|
+
issue.team,
|
|
333
|
+
issue.project,
|
|
334
|
+
issue.labels(),
|
|
335
|
+
]);
|
|
336
|
+
issues.push({
|
|
337
|
+
id: issue.id,
|
|
338
|
+
identifier: issue.identifier,
|
|
339
|
+
url: issue.url,
|
|
340
|
+
title: issue.title,
|
|
341
|
+
description: issue.description || undefined,
|
|
342
|
+
priority: issue.priority,
|
|
343
|
+
estimate: issue.estimate || undefined,
|
|
344
|
+
state: state ? { id: state.id, name: state.name } : undefined,
|
|
345
|
+
assignee: assignee
|
|
346
|
+
? { id: assignee.id, name: assignee.name }
|
|
347
|
+
: undefined,
|
|
348
|
+
team: issueTeam
|
|
349
|
+
? { id: issueTeam.id, key: issueTeam.key, name: issueTeam.name }
|
|
350
|
+
: undefined,
|
|
351
|
+
project: project ? { id: project.id, name: project.name } : undefined,
|
|
352
|
+
labels: labels.nodes.map((label) => ({
|
|
353
|
+
id: label.id,
|
|
354
|
+
name: label.name,
|
|
355
|
+
})),
|
|
356
|
+
createdAt: toISOStringOrNow(issue.createdAt),
|
|
357
|
+
updatedAt: toISOStringOrNow(issue.updatedAt),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
id: cycle.id,
|
|
362
|
+
name: cycle.name ?? undefined,
|
|
363
|
+
number: cycle.number,
|
|
364
|
+
startsAt: toISOStringOrUndefined(cycle.startsAt),
|
|
365
|
+
endsAt: toISOStringOrUndefined(cycle.endsAt),
|
|
366
|
+
isActive: cycle.isActive,
|
|
367
|
+
progress: cycle.progress,
|
|
368
|
+
issueCountHistory: cycle.issueCountHistory,
|
|
369
|
+
team: team ? { id: team.id, key: team.key, name: team.name } : undefined,
|
|
370
|
+
issues,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
async resolveCycleId(cycleNameOrId, teamFilter) {
|
|
374
|
+
if (isUuid(cycleNameOrId)) {
|
|
375
|
+
return cycleNameOrId;
|
|
376
|
+
}
|
|
377
|
+
const filter = {
|
|
378
|
+
name: { eq: cycleNameOrId },
|
|
379
|
+
};
|
|
380
|
+
if (teamFilter) {
|
|
381
|
+
const teamId = await this.resolveTeamId(teamFilter);
|
|
382
|
+
filter.team = teamIdFilter(teamId);
|
|
383
|
+
}
|
|
384
|
+
const cyclesConnection = await this.client.cycles({
|
|
385
|
+
filter,
|
|
386
|
+
first: 10,
|
|
387
|
+
});
|
|
388
|
+
const cyclesData = cyclesConnection.nodes;
|
|
389
|
+
const nodes = [];
|
|
390
|
+
for (const cycle of cyclesData) {
|
|
391
|
+
const team = await cycle.team;
|
|
392
|
+
nodes.push({
|
|
393
|
+
id: cycle.id,
|
|
394
|
+
name: cycle.name ?? undefined,
|
|
395
|
+
number: cycle.number,
|
|
396
|
+
startsAt: toISOStringOrUndefined(cycle.startsAt),
|
|
397
|
+
isActive: cycle.isActive,
|
|
398
|
+
isNext: cycle.isNext,
|
|
399
|
+
isPrevious: cycle.isPrevious,
|
|
400
|
+
team: team
|
|
401
|
+
? { id: team.id, key: team.key, name: team.name }
|
|
402
|
+
: undefined,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (nodes.length === 0) {
|
|
406
|
+
throw notFoundError("Cycle", cycleNameOrId, teamFilter ? `for team ${teamFilter}` : undefined);
|
|
407
|
+
}
|
|
408
|
+
let chosen = nodes.find((n) => n.isActive);
|
|
409
|
+
if (!chosen) {
|
|
410
|
+
chosen = nodes.find((n) => n.isNext);
|
|
411
|
+
}
|
|
412
|
+
if (!chosen) {
|
|
413
|
+
chosen = nodes.find((n) => n.isPrevious);
|
|
414
|
+
}
|
|
415
|
+
if (!chosen && nodes.length === 1) {
|
|
416
|
+
chosen = nodes[0];
|
|
417
|
+
}
|
|
418
|
+
if (!chosen) {
|
|
419
|
+
const matches = nodes.map((n) => `${n.id} (${n.team?.key || "?"} / #${n.number} / ${n.startsAt})`);
|
|
420
|
+
throw multipleMatchesError("cycle", cycleNameOrId, matches, "use an ID or scope with --team");
|
|
421
|
+
}
|
|
422
|
+
return chosen.id;
|
|
423
|
+
}
|
|
424
|
+
async resolveProjectId(projectNameOrId) {
|
|
425
|
+
if (isUuid(projectNameOrId)) {
|
|
426
|
+
return projectNameOrId;
|
|
427
|
+
}
|
|
428
|
+
const filter = { name: { eqIgnoreCase: projectNameOrId } };
|
|
429
|
+
const projectsConnection = await this.client.projects({
|
|
430
|
+
filter,
|
|
431
|
+
first: 1,
|
|
432
|
+
});
|
|
433
|
+
if (projectsConnection.nodes.length === 0) {
|
|
434
|
+
throw notFoundError("Project", projectNameOrId);
|
|
435
|
+
}
|
|
436
|
+
return projectsConnection.nodes[0].id;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
export function createLinearService(options) {
|
|
440
|
+
const apiToken = getApiToken(options);
|
|
441
|
+
return new LinearService(apiToken);
|
|
442
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight markdown → ProseMirror document converter.
|
|
3
|
+
*
|
|
4
|
+
* Handles the subset of markdown commonly used in Linear comments:
|
|
5
|
+
* headings, bullet/ordered lists, code blocks, blockquotes, horizontal rules,
|
|
6
|
+
* and inline marks (bold, italic, code, links).
|
|
7
|
+
*/
|
|
8
|
+
interface ProseMirrorMark {
|
|
9
|
+
attrs?: Record<string, unknown>;
|
|
10
|
+
type: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProseMirrorNode {
|
|
13
|
+
attrs?: Record<string, unknown>;
|
|
14
|
+
content?: ProseMirrorNode[];
|
|
15
|
+
marks?: ProseMirrorMark[];
|
|
16
|
+
text?: string;
|
|
17
|
+
type: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function markdownToProseMirror(text: string): ProseMirrorNode;
|
|
20
|
+
/**
|
|
21
|
+
* Parse inline markdown (bold, italic, code, links) into ProseMirror nodes with marks.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseInline(text: string): ProseMirrorNode[];
|
|
24
|
+
export {};
|