@fink-andreas/pi-linear-tools 0.4.2 → 0.4.3
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/CHANGELOG.md +16 -0
- package/README.md +131 -3
- package/extensions/pi-linear-tools.js +180 -10
- package/index.js +1 -1019
- package/package.json +2 -2
- package/src/cli.js +700 -6
- package/src/handlers.js +403 -0
- package/src/linear.js +1218 -9
- package/src/sync-doc.js +1208 -0
package/src/linear.js
CHANGED
|
@@ -90,6 +90,360 @@ const ISSUES_WITH_RELATIONS_QUERY = `
|
|
|
90
90
|
}
|
|
91
91
|
`;
|
|
92
92
|
|
|
93
|
+
const PROJECT_DETAILS_QUERY = `
|
|
94
|
+
query ProjectDetails($id: String!, $milestoneLimit: Int!) {
|
|
95
|
+
project(id: $id) {
|
|
96
|
+
id
|
|
97
|
+
name
|
|
98
|
+
description
|
|
99
|
+
content
|
|
100
|
+
color
|
|
101
|
+
icon
|
|
102
|
+
priority
|
|
103
|
+
progress
|
|
104
|
+
health
|
|
105
|
+
startDate
|
|
106
|
+
targetDate
|
|
107
|
+
slugId
|
|
108
|
+
url
|
|
109
|
+
archivedAt
|
|
110
|
+
completedAt
|
|
111
|
+
canceledAt
|
|
112
|
+
status {
|
|
113
|
+
id
|
|
114
|
+
name
|
|
115
|
+
type
|
|
116
|
+
color
|
|
117
|
+
}
|
|
118
|
+
lead {
|
|
119
|
+
id
|
|
120
|
+
name
|
|
121
|
+
displayName
|
|
122
|
+
}
|
|
123
|
+
teams {
|
|
124
|
+
nodes {
|
|
125
|
+
id
|
|
126
|
+
key
|
|
127
|
+
name
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
projectMilestones(first: $milestoneLimit) {
|
|
131
|
+
nodes {
|
|
132
|
+
id
|
|
133
|
+
name
|
|
134
|
+
status
|
|
135
|
+
progress
|
|
136
|
+
targetDate
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const PROJECTS_LOOKUP_QUERY = `
|
|
144
|
+
query ProjectsLookup($includeArchived: Boolean!) {
|
|
145
|
+
projects(first: 250, includeArchived: $includeArchived) {
|
|
146
|
+
nodes {
|
|
147
|
+
id
|
|
148
|
+
name
|
|
149
|
+
slugId
|
|
150
|
+
archivedAt
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
const PROJECT_CREATE_MUTATION = `
|
|
157
|
+
mutation ProjectCreate($input: ProjectCreateInput!) {
|
|
158
|
+
projectCreate(input: $input) {
|
|
159
|
+
success
|
|
160
|
+
project {
|
|
161
|
+
id
|
|
162
|
+
name
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const PROJECT_UPDATE_MUTATION = `
|
|
169
|
+
mutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {
|
|
170
|
+
projectUpdate(id: $id, input: $input) {
|
|
171
|
+
success
|
|
172
|
+
project {
|
|
173
|
+
id
|
|
174
|
+
name
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
const PROJECT_DELETE_MUTATION = `
|
|
181
|
+
mutation ProjectDelete($id: String!) {
|
|
182
|
+
projectDelete(id: $id) {
|
|
183
|
+
success
|
|
184
|
+
entity {
|
|
185
|
+
id
|
|
186
|
+
name
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const PROJECT_ARCHIVE_MUTATION = `
|
|
193
|
+
mutation ProjectArchive($id: String!) {
|
|
194
|
+
projectArchiveResult: projectDelete(id: $id) {
|
|
195
|
+
success
|
|
196
|
+
entity {
|
|
197
|
+
id
|
|
198
|
+
name
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
const PROJECT_UNARCHIVE_MUTATION = `
|
|
205
|
+
mutation ProjectUnarchive($id: String!) {
|
|
206
|
+
projectUnarchive(id: $id) {
|
|
207
|
+
success
|
|
208
|
+
entity {
|
|
209
|
+
id
|
|
210
|
+
name
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
const PROJECT_UPDATES_BY_PROJECT_QUERY = `
|
|
217
|
+
query ProjectUpdatesByProject($id: String!, $first: Int!, $includeArchived: Boolean!) {
|
|
218
|
+
project(id: $id) {
|
|
219
|
+
id
|
|
220
|
+
name
|
|
221
|
+
projectUpdates(first: $first, includeArchived: $includeArchived) {
|
|
222
|
+
nodes {
|
|
223
|
+
id
|
|
224
|
+
body
|
|
225
|
+
health
|
|
226
|
+
createdAt
|
|
227
|
+
updatedAt
|
|
228
|
+
archivedAt
|
|
229
|
+
url
|
|
230
|
+
slugId
|
|
231
|
+
isDiffHidden
|
|
232
|
+
isStale
|
|
233
|
+
user {
|
|
234
|
+
id
|
|
235
|
+
name
|
|
236
|
+
displayName
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
const PROJECT_UPDATE_DETAILS_QUERY = `
|
|
245
|
+
query ProjectUpdateDetails($id: String!) {
|
|
246
|
+
projectUpdate(id: $id) {
|
|
247
|
+
id
|
|
248
|
+
body
|
|
249
|
+
health
|
|
250
|
+
createdAt
|
|
251
|
+
updatedAt
|
|
252
|
+
archivedAt
|
|
253
|
+
editedAt
|
|
254
|
+
url
|
|
255
|
+
slugId
|
|
256
|
+
isDiffHidden
|
|
257
|
+
isStale
|
|
258
|
+
project {
|
|
259
|
+
id
|
|
260
|
+
name
|
|
261
|
+
}
|
|
262
|
+
user {
|
|
263
|
+
id
|
|
264
|
+
name
|
|
265
|
+
displayName
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
const PROJECT_UPDATE_CREATE_MUTATION = `
|
|
272
|
+
mutation ProjectUpdateCreate($input: ProjectUpdateCreateInput!) {
|
|
273
|
+
projectUpdateCreate(input: $input) {
|
|
274
|
+
success
|
|
275
|
+
projectUpdate {
|
|
276
|
+
id
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
const PROJECT_UPDATE_UPDATE_MUTATION = `
|
|
283
|
+
mutation ProjectUpdateUpdate($id: String!, $input: ProjectUpdateUpdateInput!) {
|
|
284
|
+
projectUpdateUpdate(id: $id, input: $input) {
|
|
285
|
+
success
|
|
286
|
+
projectUpdate {
|
|
287
|
+
id
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
const PROJECT_UPDATE_ARCHIVE_MUTATION = `
|
|
294
|
+
mutation ProjectUpdateArchive($id: String!) {
|
|
295
|
+
projectUpdateArchive(id: $id) {
|
|
296
|
+
success
|
|
297
|
+
entity {
|
|
298
|
+
id
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
`;
|
|
303
|
+
|
|
304
|
+
const PROJECT_UPDATE_UNARCHIVE_MUTATION = `
|
|
305
|
+
mutation ProjectUpdateUnarchive($id: String!) {
|
|
306
|
+
projectUpdateUnarchive(id: $id) {
|
|
307
|
+
success
|
|
308
|
+
entity {
|
|
309
|
+
id
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
const DOCUMENT_DETAILS_QUERY = `
|
|
316
|
+
query DocumentDetails($id: String!) {
|
|
317
|
+
document(id: $id) {
|
|
318
|
+
id
|
|
319
|
+
title
|
|
320
|
+
content
|
|
321
|
+
icon
|
|
322
|
+
color
|
|
323
|
+
slugId
|
|
324
|
+
url
|
|
325
|
+
archivedAt
|
|
326
|
+
createdAt
|
|
327
|
+
updatedAt
|
|
328
|
+
project {
|
|
329
|
+
id
|
|
330
|
+
name
|
|
331
|
+
}
|
|
332
|
+
issue {
|
|
333
|
+
id
|
|
334
|
+
identifier
|
|
335
|
+
title
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
`;
|
|
340
|
+
|
|
341
|
+
const DOCUMENT_CREATE_MUTATION = `
|
|
342
|
+
mutation DocumentCreate($input: DocumentCreateInput!) {
|
|
343
|
+
documentCreate(input: $input) {
|
|
344
|
+
success
|
|
345
|
+
document {
|
|
346
|
+
id
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
`;
|
|
351
|
+
|
|
352
|
+
const DOCUMENT_UPDATE_MUTATION = `
|
|
353
|
+
mutation DocumentUpdate($id: String!, $input: DocumentUpdateInput!) {
|
|
354
|
+
documentUpdate(id: $id, input: $input) {
|
|
355
|
+
success
|
|
356
|
+
document {
|
|
357
|
+
id
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
`;
|
|
362
|
+
|
|
363
|
+
const ISSUE_ACTIVITY_QUERY = `
|
|
364
|
+
query IssueActivity($id: String!, $first: Int!, $includeArchived: Boolean!) {
|
|
365
|
+
issue(id: $id) {
|
|
366
|
+
id
|
|
367
|
+
identifier
|
|
368
|
+
title
|
|
369
|
+
url
|
|
370
|
+
history(first: $first, includeArchived: $includeArchived) {
|
|
371
|
+
nodes {
|
|
372
|
+
id
|
|
373
|
+
createdAt
|
|
374
|
+
updatedAt
|
|
375
|
+
archived
|
|
376
|
+
archivedAt
|
|
377
|
+
autoArchived
|
|
378
|
+
autoClosed
|
|
379
|
+
trashed
|
|
380
|
+
updatedDescription
|
|
381
|
+
fromTitle
|
|
382
|
+
toTitle
|
|
383
|
+
fromPriority
|
|
384
|
+
toPriority
|
|
385
|
+
fromState {
|
|
386
|
+
id
|
|
387
|
+
name
|
|
388
|
+
}
|
|
389
|
+
toState {
|
|
390
|
+
id
|
|
391
|
+
name
|
|
392
|
+
}
|
|
393
|
+
fromAssignee {
|
|
394
|
+
id
|
|
395
|
+
name
|
|
396
|
+
displayName
|
|
397
|
+
}
|
|
398
|
+
toAssignee {
|
|
399
|
+
id
|
|
400
|
+
name
|
|
401
|
+
displayName
|
|
402
|
+
}
|
|
403
|
+
fromProject {
|
|
404
|
+
id
|
|
405
|
+
name
|
|
406
|
+
}
|
|
407
|
+
toProject {
|
|
408
|
+
id
|
|
409
|
+
name
|
|
410
|
+
}
|
|
411
|
+
fromProjectMilestone {
|
|
412
|
+
id
|
|
413
|
+
name
|
|
414
|
+
}
|
|
415
|
+
toProjectMilestone {
|
|
416
|
+
id
|
|
417
|
+
name
|
|
418
|
+
}
|
|
419
|
+
addedLabels {
|
|
420
|
+
id
|
|
421
|
+
name
|
|
422
|
+
}
|
|
423
|
+
removedLabels {
|
|
424
|
+
id
|
|
425
|
+
name
|
|
426
|
+
}
|
|
427
|
+
relationChanges {
|
|
428
|
+
identifier
|
|
429
|
+
type
|
|
430
|
+
}
|
|
431
|
+
attachment {
|
|
432
|
+
id
|
|
433
|
+
title
|
|
434
|
+
url
|
|
435
|
+
}
|
|
436
|
+
actor {
|
|
437
|
+
id
|
|
438
|
+
name
|
|
439
|
+
displayName
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
`;
|
|
446
|
+
|
|
93
447
|
/**
|
|
94
448
|
* Execute an optimized GraphQL query using rawRequest
|
|
95
449
|
* Falls back to SDK method if rawRequest is not available (e.g., in tests)
|
|
@@ -136,6 +490,21 @@ async function executeOptimizedQuery(client, query, variables) {
|
|
|
136
490
|
};
|
|
137
491
|
}
|
|
138
492
|
|
|
493
|
+
async function executeGraphQL(client, query, variables = {}) {
|
|
494
|
+
const rawRequest =
|
|
495
|
+
(typeof client.client?.rawRequest === 'function' ? client.client.rawRequest.bind(client.client) : null) ||
|
|
496
|
+
(typeof client.rawRequest === 'function' ? client.rawRequest.bind(client) : null);
|
|
497
|
+
|
|
498
|
+
if (!rawRequest) {
|
|
499
|
+
throw new Error('GraphQL rawRequest is unavailable on this Linear client');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const response = await rawRequest(query, variables);
|
|
503
|
+
updateRateLimitState(response);
|
|
504
|
+
checkRateLimitWarning();
|
|
505
|
+
return response.data;
|
|
506
|
+
}
|
|
507
|
+
|
|
139
508
|
/**
|
|
140
509
|
* Transform raw GraphQL issue data to plain object format
|
|
141
510
|
* Used by optimized queries to avoid SDK lazy loading
|
|
@@ -379,9 +748,201 @@ function isLinearId(value) {
|
|
|
379
748
|
function normalizeIssueLookupInput(issue) {
|
|
380
749
|
const value = String(issue || '').trim();
|
|
381
750
|
if (!value) throw new Error('Missing required issue identifier');
|
|
751
|
+
|
|
752
|
+
const issueUrlMatch = value.match(/\/issue\/([A-Za-z0-9]+-\d+)(?:[/?#]|$)/i);
|
|
753
|
+
if (issueUrlMatch?.[1]) {
|
|
754
|
+
return issueUrlMatch[1].toUpperCase();
|
|
755
|
+
}
|
|
756
|
+
|
|
382
757
|
return value;
|
|
383
758
|
}
|
|
384
759
|
|
|
760
|
+
function extractProjectLookupValue(projectRef) {
|
|
761
|
+
const ref = String(projectRef || '').trim();
|
|
762
|
+
if (!ref) {
|
|
763
|
+
return '';
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const projectUrlMatch = ref.match(/\/project\/([^/?#]+)/i);
|
|
767
|
+
if (projectUrlMatch?.[1]) {
|
|
768
|
+
return projectUrlMatch[1];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return ref;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function getProjectLookupCandidates(projectRef) {
|
|
775
|
+
const lookupValue = extractProjectLookupValue(projectRef);
|
|
776
|
+
const candidates = new Set([lookupValue]);
|
|
777
|
+
|
|
778
|
+
if (lookupValue.includes('-')) {
|
|
779
|
+
const slugSuffix = lookupValue.split('-').pop();
|
|
780
|
+
if (slugSuffix) {
|
|
781
|
+
candidates.add(slugSuffix);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return Array.from(candidates).filter(Boolean);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const PROJECT_UPDATE_HEALTH_VALUES = ['onTrack', 'atRisk', 'offTrack'];
|
|
789
|
+
|
|
790
|
+
function normalizePositiveInteger(value, fieldName, defaultValue) {
|
|
791
|
+
if (value === undefined || value === null) {
|
|
792
|
+
return defaultValue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const parsed = typeof value === 'number' ? value : Number(String(value).trim());
|
|
796
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
797
|
+
throw new Error(`${fieldName} must be a positive integer`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return parsed;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function normalizeProjectUpdateHealth(value) {
|
|
804
|
+
if (value === undefined) {
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const normalized = String(value).trim();
|
|
809
|
+
if (!normalized) {
|
|
810
|
+
throw new Error(`health must be one of: ${PROJECT_UPDATE_HEALTH_VALUES.join(', ')}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (!PROJECT_UPDATE_HEALTH_VALUES.includes(normalized)) {
|
|
814
|
+
throw new Error(`health must be one of: ${PROJECT_UPDATE_HEALTH_VALUES.join(', ')}`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return normalized;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function formatPriorityLabel(value) {
|
|
821
|
+
if (value === undefined || value === null) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const numeric = Number(value);
|
|
826
|
+
if (!Number.isFinite(numeric)) {
|
|
827
|
+
return String(value);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const priorityNames = ['No priority', 'Urgent', 'High', 'Medium', 'Low'];
|
|
831
|
+
return priorityNames[numeric] || `Priority ${numeric}`;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function getUserDisplayName(user) {
|
|
835
|
+
return user?.displayName || user?.name || 'Unknown';
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function summarizeIssueHistoryEntry(entry) {
|
|
839
|
+
if (entry.fromState?.name || entry.toState?.name) {
|
|
840
|
+
if (entry.fromState?.name && entry.toState?.name) {
|
|
841
|
+
return `moved state from ${entry.fromState.name} to ${entry.toState.name}`;
|
|
842
|
+
}
|
|
843
|
+
if (entry.toState?.name) {
|
|
844
|
+
return `set state to ${entry.toState.name}`;
|
|
845
|
+
}
|
|
846
|
+
return `cleared state ${entry.fromState.name}`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (entry.fromAssignee || entry.toAssignee) {
|
|
850
|
+
const fromAssignee = entry.fromAssignee ? getUserDisplayName(entry.fromAssignee) : null;
|
|
851
|
+
const toAssignee = entry.toAssignee ? getUserDisplayName(entry.toAssignee) : null;
|
|
852
|
+
if (fromAssignee && toAssignee) {
|
|
853
|
+
return `reassigned from ${fromAssignee} to ${toAssignee}`;
|
|
854
|
+
}
|
|
855
|
+
if (toAssignee) {
|
|
856
|
+
return `assigned to ${toAssignee}`;
|
|
857
|
+
}
|
|
858
|
+
return `unassigned from ${fromAssignee}`;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (entry.fromTitle || entry.toTitle) {
|
|
862
|
+
if (entry.fromTitle && entry.toTitle) {
|
|
863
|
+
return `renamed issue from "${entry.fromTitle}" to "${entry.toTitle}"`;
|
|
864
|
+
}
|
|
865
|
+
if (entry.toTitle) {
|
|
866
|
+
return `set title to "${entry.toTitle}"`;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (entry.fromPriority !== undefined || entry.toPriority !== undefined) {
|
|
871
|
+
const fromPriority = formatPriorityLabel(entry.fromPriority);
|
|
872
|
+
const toPriority = formatPriorityLabel(entry.toPriority);
|
|
873
|
+
if (fromPriority && toPriority) {
|
|
874
|
+
return `changed priority from ${fromPriority} to ${toPriority}`;
|
|
875
|
+
}
|
|
876
|
+
if (toPriority) {
|
|
877
|
+
return `set priority to ${toPriority}`;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (entry.fromProject?.name || entry.toProject?.name) {
|
|
882
|
+
if (entry.fromProject?.name && entry.toProject?.name) {
|
|
883
|
+
return `moved project from ${entry.fromProject.name} to ${entry.toProject.name}`;
|
|
884
|
+
}
|
|
885
|
+
if (entry.toProject?.name) {
|
|
886
|
+
return `added to project ${entry.toProject.name}`;
|
|
887
|
+
}
|
|
888
|
+
return `removed from project ${entry.fromProject.name}`;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (entry.fromProjectMilestone?.name || entry.toProjectMilestone?.name) {
|
|
892
|
+
if (entry.fromProjectMilestone?.name && entry.toProjectMilestone?.name) {
|
|
893
|
+
return `moved milestone from ${entry.fromProjectMilestone.name} to ${entry.toProjectMilestone.name}`;
|
|
894
|
+
}
|
|
895
|
+
if (entry.toProjectMilestone?.name) {
|
|
896
|
+
return `set milestone to ${entry.toProjectMilestone.name}`;
|
|
897
|
+
}
|
|
898
|
+
return `cleared milestone ${entry.fromProjectMilestone.name}`;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if ((entry.addedLabels?.length || 0) > 0 || (entry.removedLabels?.length || 0) > 0) {
|
|
902
|
+
const labelChanges = [];
|
|
903
|
+
if ((entry.addedLabels?.length || 0) > 0) {
|
|
904
|
+
labelChanges.push(`added labels ${entry.addedLabels.map((label) => label.name).join(', ')}`);
|
|
905
|
+
}
|
|
906
|
+
if ((entry.removedLabels?.length || 0) > 0) {
|
|
907
|
+
labelChanges.push(`removed labels ${entry.removedLabels.map((label) => label.name).join(', ')}`);
|
|
908
|
+
}
|
|
909
|
+
return labelChanges.join('; ');
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if ((entry.relationChanges?.length || 0) > 0) {
|
|
913
|
+
const relationSummary = entry.relationChanges
|
|
914
|
+
.map((relation) => `${relation.type} ${relation.identifier}`)
|
|
915
|
+
.join(', ');
|
|
916
|
+
return `updated relations: ${relationSummary}`;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (entry.updatedDescription) {
|
|
920
|
+
return 'updated description';
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (entry.attachment?.title || entry.attachment?.url) {
|
|
924
|
+
return `linked attachment ${entry.attachment.title ? `"${entry.attachment.title}"` : entry.attachment.url}`;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (entry.archivedAt || entry.archived === true) {
|
|
928
|
+
return entry.autoArchived ? 'auto-archived issue' : 'archived issue';
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (entry.autoClosed) {
|
|
932
|
+
return 'auto-closed issue';
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (entry.trashed === true) {
|
|
936
|
+
return 'trashed issue';
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (entry.trashed === false) {
|
|
940
|
+
return 'restored issue from trash';
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return 'updated issue';
|
|
944
|
+
}
|
|
945
|
+
|
|
385
946
|
/**
|
|
386
947
|
* Safely resolve a lazy-loaded relation without triggering unnecessary API calls
|
|
387
948
|
* Only fetches if the relation is already cached or if we have minimal data
|
|
@@ -686,28 +1247,65 @@ export async function fetchIssuesByProject(client, projectId, states, options =
|
|
|
686
1247
|
/**
|
|
687
1248
|
* Fetch all accessible projects from Linear API
|
|
688
1249
|
* @param {LinearClient} client - Linear SDK client
|
|
1250
|
+
* @param {{ includeArchived?: boolean }} options - Fetch options
|
|
689
1251
|
* @returns {Promise<Array<{id: string, name: string}>>}
|
|
690
1252
|
*/
|
|
691
|
-
export async function fetchProjects(client) {
|
|
1253
|
+
export async function fetchProjects(client, options = {}) {
|
|
692
1254
|
return withLinearErrorHandling(async () => {
|
|
1255
|
+
const { includeArchived = false, forceGraphql = false } = options;
|
|
693
1256
|
const cacheKey = getClientCacheKey(client);
|
|
694
|
-
const
|
|
1257
|
+
const scopedCacheKey = `${cacheKey}::projects::${includeArchived ? 'all' : 'active'}::${forceGraphql ? 'graphql' : 'sdk'}`;
|
|
1258
|
+
const cached = getCache(projectsCache, scopedCacheKey);
|
|
695
1259
|
if (cached) return cached;
|
|
696
1260
|
|
|
697
|
-
|
|
698
|
-
|
|
1261
|
+
let nodes = [];
|
|
1262
|
+
if (includeArchived || forceGraphql) {
|
|
1263
|
+
const data = await executeGraphQL(client, PROJECTS_LOOKUP_QUERY, {
|
|
1264
|
+
includeArchived,
|
|
1265
|
+
});
|
|
1266
|
+
nodes = data?.projects?.nodes ?? [];
|
|
1267
|
+
} else {
|
|
1268
|
+
const result = await client.projects();
|
|
1269
|
+
nodes = result.nodes ?? [];
|
|
1270
|
+
}
|
|
699
1271
|
|
|
700
1272
|
debug('Fetched Linear projects', {
|
|
701
1273
|
projectCount: nodes.length,
|
|
1274
|
+
includeArchived,
|
|
702
1275
|
projects: nodes.map((p) => ({ id: p.id, name: p.name })),
|
|
703
1276
|
});
|
|
704
1277
|
|
|
705
|
-
const projects = nodes.map(p => ({
|
|
706
|
-
|
|
1278
|
+
const projects = nodes.map((p) => ({
|
|
1279
|
+
id: p.id,
|
|
1280
|
+
name: p.name,
|
|
1281
|
+
slugId: p.slugId ?? null,
|
|
1282
|
+
archivedAt: p.archivedAt ?? null,
|
|
1283
|
+
}));
|
|
1284
|
+
setCache(projectsCache, scopedCacheKey, projects, CACHE_TTL_MS.projects);
|
|
707
1285
|
return projects;
|
|
708
1286
|
}, 'fetchProjects');
|
|
709
1287
|
}
|
|
710
1288
|
|
|
1289
|
+
export async function fetchProjectDetails(client, projectRef, options = {}) {
|
|
1290
|
+
return withLinearErrorHandling(async () => {
|
|
1291
|
+
const { milestoneLimit = 10 } = options;
|
|
1292
|
+
const ref = String(projectRef || '').trim();
|
|
1293
|
+
const projectId = isLinearId(ref)
|
|
1294
|
+
? ref
|
|
1295
|
+
: (await resolveProjectRef(client, ref)).id;
|
|
1296
|
+
const data = await executeGraphQL(client, PROJECT_DETAILS_QUERY, {
|
|
1297
|
+
id: projectId,
|
|
1298
|
+
milestoneLimit,
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
if (!data?.project) {
|
|
1302
|
+
throw new Error(`Project not found: ${projectRef}`);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return transformProject(data.project);
|
|
1306
|
+
}, 'fetchProjectDetails');
|
|
1307
|
+
}
|
|
1308
|
+
|
|
711
1309
|
/**
|
|
712
1310
|
* Fetch available workspaces (organization context) from Linear API
|
|
713
1311
|
* @param {LinearClient} client - Linear SDK client
|
|
@@ -871,10 +1469,12 @@ export async function getTeamWorkflowStates(client, teamRef) {
|
|
|
871
1469
|
* Resolve a project reference (name or ID) to a project object
|
|
872
1470
|
* @param {LinearClient} client - Linear SDK client
|
|
873
1471
|
* @param {string} projectRef - Project name or ID
|
|
1472
|
+
* @param {{ includeArchived?: boolean }} options - Lookup options
|
|
874
1473
|
* @returns {Promise<{id: string, name: string}>}
|
|
875
1474
|
*/
|
|
876
|
-
export async function resolveProjectRef(client, projectRef) {
|
|
1475
|
+
export async function resolveProjectRef(client, projectRef, options = {}) {
|
|
877
1476
|
const ref = String(projectRef || '').trim();
|
|
1477
|
+
const { includeArchived = false } = options;
|
|
878
1478
|
if (!ref) {
|
|
879
1479
|
throw new Error('Missing project reference');
|
|
880
1480
|
}
|
|
@@ -890,7 +1490,7 @@ export async function resolveProjectRef(client, projectRef) {
|
|
|
890
1490
|
// fall back to cached/full-project lookup below
|
|
891
1491
|
}
|
|
892
1492
|
|
|
893
|
-
const projectsById = await fetchProjects(client);
|
|
1493
|
+
const projectsById = await fetchProjects(client, { includeArchived });
|
|
894
1494
|
const byId = projectsById.find((p) => p.id === ref);
|
|
895
1495
|
if (byId) {
|
|
896
1496
|
return byId;
|
|
@@ -898,7 +1498,13 @@ export async function resolveProjectRef(client, projectRef) {
|
|
|
898
1498
|
throw new Error(`Project not found with ID: ${ref}`);
|
|
899
1499
|
}
|
|
900
1500
|
|
|
901
|
-
const
|
|
1501
|
+
const lookupCandidates = getProjectLookupCandidates(ref);
|
|
1502
|
+
const lookupValue = lookupCandidates[0] || ref;
|
|
1503
|
+
const shouldUseGraphqlLookup = lookupValue !== ref;
|
|
1504
|
+
const projects = await fetchProjects(client, {
|
|
1505
|
+
includeArchived,
|
|
1506
|
+
forceGraphql: shouldUseGraphqlLookup,
|
|
1507
|
+
});
|
|
902
1508
|
|
|
903
1509
|
// Try exact name match
|
|
904
1510
|
const exactName = projects.find((p) => p.name === ref);
|
|
@@ -906,6 +1512,12 @@ export async function resolveProjectRef(client, projectRef) {
|
|
|
906
1512
|
return exactName;
|
|
907
1513
|
}
|
|
908
1514
|
|
|
1515
|
+
// Try exact slug match
|
|
1516
|
+
const exactSlug = projects.find((p) => lookupCandidates.includes(p.slugId));
|
|
1517
|
+
if (exactSlug) {
|
|
1518
|
+
return exactSlug;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
909
1521
|
// Try case-insensitive name match
|
|
910
1522
|
const lowerRef = ref.toLowerCase();
|
|
911
1523
|
const insensitiveName = projects.find((p) => p.name?.toLowerCase() === lowerRef);
|
|
@@ -913,9 +1525,398 @@ export async function resolveProjectRef(client, projectRef) {
|
|
|
913
1525
|
return insensitiveName;
|
|
914
1526
|
}
|
|
915
1527
|
|
|
1528
|
+
// Try case-insensitive slug match
|
|
1529
|
+
const lowerLookupValues = lookupCandidates.map((candidate) => candidate.toLowerCase());
|
|
1530
|
+
const insensitiveSlug = projects.find((p) => p.slugId && lowerLookupValues.includes(p.slugId.toLowerCase()));
|
|
1531
|
+
if (insensitiveSlug) {
|
|
1532
|
+
return insensitiveSlug;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
916
1535
|
throw new Error(`Project not found: ${ref}. Available projects: ${projects.map((p) => p.name).join(', ')}`);
|
|
917
1536
|
}
|
|
918
1537
|
|
|
1538
|
+
export async function createProject(client, input) {
|
|
1539
|
+
return withLinearErrorHandling(async () => {
|
|
1540
|
+
const name = String(input.name || '').trim();
|
|
1541
|
+
if (!name) {
|
|
1542
|
+
throw new Error('Missing required field: name');
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const teamIds = Array.isArray(input.teamIds)
|
|
1546
|
+
? input.teamIds.map((value) => String(value || '').trim()).filter(Boolean)
|
|
1547
|
+
: [];
|
|
1548
|
+
|
|
1549
|
+
if (teamIds.length === 0) {
|
|
1550
|
+
throw new Error('Missing required field: teamIds');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const createInput = {
|
|
1554
|
+
name,
|
|
1555
|
+
teamIds,
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
for (const field of ['description', 'color', 'icon', 'leadId', 'startDate', 'targetDate']) {
|
|
1559
|
+
if (input[field] !== undefined) {
|
|
1560
|
+
createInput[field] = input[field];
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (input.priority !== undefined) {
|
|
1565
|
+
createInput.priority = input.priority;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const payload = await executeGraphQL(client, PROJECT_CREATE_MUTATION, {
|
|
1569
|
+
input: createInput,
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
if (!payload?.projectCreate?.success || !payload?.projectCreate?.project?.id) {
|
|
1573
|
+
throw new Error('Failed to create project');
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
invalidateProjectsCache(client);
|
|
1577
|
+
return fetchProjectDetails(client, payload.projectCreate.project.id);
|
|
1578
|
+
}, 'createProject');
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
export async function updateProject(client, projectRef, patch = {}) {
|
|
1582
|
+
return withLinearErrorHandling(async () => {
|
|
1583
|
+
const resolved = await resolveProjectRef(client, projectRef);
|
|
1584
|
+
const updateInput = {};
|
|
1585
|
+
|
|
1586
|
+
for (const field of ['name', 'description', 'content', 'color', 'icon', 'startDate', 'targetDate']) {
|
|
1587
|
+
if (patch[field] !== undefined) {
|
|
1588
|
+
updateInput[field] = patch[field];
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
if (patch.priority !== undefined) {
|
|
1593
|
+
updateInput.priority = patch.priority;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
if (patch.leadId !== undefined) {
|
|
1597
|
+
updateInput.leadId = patch.leadId;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (patch.teamIds !== undefined) {
|
|
1601
|
+
updateInput.teamIds = patch.teamIds;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (Object.keys(updateInput).length === 0) {
|
|
1605
|
+
throw new Error('No update fields provided');
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_MUTATION, {
|
|
1609
|
+
id: resolved.id,
|
|
1610
|
+
input: updateInput,
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
if (!payload?.projectUpdate?.success || !payload?.projectUpdate?.project?.id) {
|
|
1614
|
+
throw new Error('Failed to update project');
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
invalidateProjectsCache(client);
|
|
1618
|
+
const project = await fetchProjectDetails(client, payload.projectUpdate.project.id);
|
|
1619
|
+
|
|
1620
|
+
return {
|
|
1621
|
+
project,
|
|
1622
|
+
changed: Object.keys(updateInput),
|
|
1623
|
+
};
|
|
1624
|
+
}, 'updateProject');
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
export async function deleteProject(client, projectRef) {
|
|
1628
|
+
return withLinearErrorHandling(async () => {
|
|
1629
|
+
const resolved = await resolveProjectRef(client, projectRef);
|
|
1630
|
+
const payload = await executeGraphQL(client, PROJECT_DELETE_MUTATION, {
|
|
1631
|
+
id: resolved.id,
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
if (!payload?.projectDelete?.success) {
|
|
1635
|
+
throw new Error('Failed to delete project');
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
invalidateProjectsCache(client);
|
|
1639
|
+
|
|
1640
|
+
return {
|
|
1641
|
+
success: true,
|
|
1642
|
+
projectId: resolved.id,
|
|
1643
|
+
name: resolved.name,
|
|
1644
|
+
entity: transformProject(payload.projectDelete.entity),
|
|
1645
|
+
};
|
|
1646
|
+
}, 'deleteProject');
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
export async function archiveProject(client, projectRef) {
|
|
1650
|
+
return withLinearErrorHandling(async () => {
|
|
1651
|
+
const resolved = await resolveProjectRef(client, projectRef);
|
|
1652
|
+
const payload = await executeGraphQL(client, PROJECT_ARCHIVE_MUTATION, {
|
|
1653
|
+
id: resolved.id,
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
if (!payload?.projectArchiveResult?.success) {
|
|
1657
|
+
throw new Error('Failed to archive project');
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
invalidateProjectsCache(client);
|
|
1661
|
+
|
|
1662
|
+
return {
|
|
1663
|
+
success: true,
|
|
1664
|
+
projectId: resolved.id,
|
|
1665
|
+
name: resolved.name,
|
|
1666
|
+
entity: transformProject(payload.projectArchiveResult.entity),
|
|
1667
|
+
};
|
|
1668
|
+
}, 'archiveProject');
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
export async function unarchiveProject(client, projectRef) {
|
|
1672
|
+
return withLinearErrorHandling(async () => {
|
|
1673
|
+
const ref = String(projectRef || '').trim();
|
|
1674
|
+
const resolved = isLinearId(ref)
|
|
1675
|
+
? { id: ref, name: null }
|
|
1676
|
+
: await resolveProjectRef(client, ref, { includeArchived: true });
|
|
1677
|
+
|
|
1678
|
+
const payload = await executeGraphQL(client, PROJECT_UNARCHIVE_MUTATION, {
|
|
1679
|
+
id: resolved.id,
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
if (!payload?.projectUnarchive?.success) {
|
|
1683
|
+
throw new Error('Failed to unarchive project');
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
invalidateProjectsCache(client);
|
|
1687
|
+
const project = await fetchProjectDetails(client, resolved.id);
|
|
1688
|
+
|
|
1689
|
+
return {
|
|
1690
|
+
success: true,
|
|
1691
|
+
project,
|
|
1692
|
+
};
|
|
1693
|
+
}, 'unarchiveProject');
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
export async function fetchProjectUpdates(client, projectRef, options = {}) {
|
|
1697
|
+
return withLinearErrorHandling(async () => {
|
|
1698
|
+
const includeArchived = options.includeArchived === true;
|
|
1699
|
+
const limit = normalizePositiveInteger(options.limit, 'limit', 10);
|
|
1700
|
+
const resolved = await resolveProjectRef(client, projectRef, { includeArchived });
|
|
1701
|
+
const data = await executeGraphQL(client, PROJECT_UPDATES_BY_PROJECT_QUERY, {
|
|
1702
|
+
id: resolved.id,
|
|
1703
|
+
first: limit,
|
|
1704
|
+
includeArchived,
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
const nodes = data?.project?.projectUpdates?.nodes || [];
|
|
1708
|
+
return {
|
|
1709
|
+
project: {
|
|
1710
|
+
id: data?.project?.id || resolved.id,
|
|
1711
|
+
name: data?.project?.name || resolved.name,
|
|
1712
|
+
},
|
|
1713
|
+
updates: nodes.map(transformProjectUpdate),
|
|
1714
|
+
};
|
|
1715
|
+
}, 'fetchProjectUpdates');
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
export async function fetchProjectUpdateDetails(client, projectUpdateId) {
|
|
1719
|
+
return withLinearErrorHandling(async () => {
|
|
1720
|
+
const id = String(projectUpdateId || '').trim();
|
|
1721
|
+
if (!id) {
|
|
1722
|
+
throw new Error('Missing required field: projectUpdate');
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
const data = await executeGraphQL(client, PROJECT_UPDATE_DETAILS_QUERY, { id });
|
|
1726
|
+
|
|
1727
|
+
if (!data?.projectUpdate) {
|
|
1728
|
+
throw new Error(`Project update not found: ${id}`);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
return transformProjectUpdate(data.projectUpdate);
|
|
1732
|
+
}, 'fetchProjectUpdateDetails');
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
export async function createProjectUpdate(client, input) {
|
|
1736
|
+
return withLinearErrorHandling(async () => {
|
|
1737
|
+
const projectId = String(input.projectId || '').trim();
|
|
1738
|
+
if (!projectId) {
|
|
1739
|
+
throw new Error('Missing required field: projectId');
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const createInput = { projectId };
|
|
1743
|
+
if (input.body !== undefined) createInput.body = String(input.body);
|
|
1744
|
+
if (input.health !== undefined) createInput.health = normalizeProjectUpdateHealth(input.health);
|
|
1745
|
+
if (input.isDiffHidden !== undefined) createInput.isDiffHidden = input.isDiffHidden;
|
|
1746
|
+
|
|
1747
|
+
if (createInput.body === undefined && createInput.health === undefined) {
|
|
1748
|
+
throw new Error('At least one of body or health is required');
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_CREATE_MUTATION, {
|
|
1752
|
+
input: createInput,
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
if (!payload?.projectUpdateCreate?.success || !payload?.projectUpdateCreate?.projectUpdate?.id) {
|
|
1756
|
+
throw new Error('Failed to create project update');
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
return fetchProjectUpdateDetails(client, payload.projectUpdateCreate.projectUpdate.id);
|
|
1760
|
+
}, 'createProjectUpdate');
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
export async function updateProjectUpdate(client, projectUpdateId, patch = {}) {
|
|
1764
|
+
return withLinearErrorHandling(async () => {
|
|
1765
|
+
const id = String(projectUpdateId || '').trim();
|
|
1766
|
+
if (!id) {
|
|
1767
|
+
throw new Error('Missing required field: projectUpdate');
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const updateInput = {};
|
|
1771
|
+
if (patch.body !== undefined) updateInput.body = String(patch.body);
|
|
1772
|
+
if (patch.health !== undefined) updateInput.health = normalizeProjectUpdateHealth(patch.health);
|
|
1773
|
+
if (patch.isDiffHidden !== undefined) updateInput.isDiffHidden = patch.isDiffHidden;
|
|
1774
|
+
|
|
1775
|
+
if (Object.keys(updateInput).length === 0) {
|
|
1776
|
+
throw new Error('No update fields provided');
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_UPDATE_MUTATION, {
|
|
1780
|
+
id,
|
|
1781
|
+
input: updateInput,
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
if (!payload?.projectUpdateUpdate?.success || !payload?.projectUpdateUpdate?.projectUpdate?.id) {
|
|
1785
|
+
throw new Error('Failed to update project update');
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const projectUpdate = await fetchProjectUpdateDetails(client, payload.projectUpdateUpdate.projectUpdate.id);
|
|
1789
|
+
return {
|
|
1790
|
+
projectUpdate,
|
|
1791
|
+
changed: Object.keys(updateInput),
|
|
1792
|
+
};
|
|
1793
|
+
}, 'updateProjectUpdate');
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
export async function archiveProjectUpdate(client, projectUpdateId) {
|
|
1797
|
+
return withLinearErrorHandling(async () => {
|
|
1798
|
+
const id = String(projectUpdateId || '').trim();
|
|
1799
|
+
if (!id) {
|
|
1800
|
+
throw new Error('Missing required field: projectUpdate');
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_ARCHIVE_MUTATION, { id });
|
|
1804
|
+
if (!payload?.projectUpdateArchive?.success) {
|
|
1805
|
+
throw new Error('Failed to archive project update');
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
return {
|
|
1809
|
+
success: true,
|
|
1810
|
+
projectUpdateId: id,
|
|
1811
|
+
};
|
|
1812
|
+
}, 'archiveProjectUpdate');
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
export async function unarchiveProjectUpdate(client, projectUpdateId) {
|
|
1816
|
+
return withLinearErrorHandling(async () => {
|
|
1817
|
+
const id = String(projectUpdateId || '').trim();
|
|
1818
|
+
if (!id) {
|
|
1819
|
+
throw new Error('Missing required field: projectUpdate');
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_UNARCHIVE_MUTATION, { id });
|
|
1823
|
+
if (!payload?.projectUpdateUnarchive?.success) {
|
|
1824
|
+
throw new Error('Failed to unarchive project update');
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
const projectUpdate = await fetchProjectUpdateDetails(client, id);
|
|
1828
|
+
return {
|
|
1829
|
+
success: true,
|
|
1830
|
+
projectUpdate,
|
|
1831
|
+
};
|
|
1832
|
+
}, 'unarchiveProjectUpdate');
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
export async function fetchDocumentDetails(client, documentRef) {
|
|
1836
|
+
return withLinearErrorHandling(async () => {
|
|
1837
|
+
const id = String(documentRef || '').trim();
|
|
1838
|
+
if (!id) {
|
|
1839
|
+
throw new Error('Missing required field: document');
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const data = await executeGraphQL(client, DOCUMENT_DETAILS_QUERY, { id });
|
|
1843
|
+
|
|
1844
|
+
if (!data?.document) {
|
|
1845
|
+
throw new Error(`Document not found: ${id}`);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
return transformDocument(data.document);
|
|
1849
|
+
}, 'fetchDocumentDetails');
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
export async function createDocument(client, input = {}) {
|
|
1853
|
+
return withLinearErrorHandling(async () => {
|
|
1854
|
+
const title = String(input.title || '').trim();
|
|
1855
|
+
if (!title) {
|
|
1856
|
+
throw new Error('Missing required field: title');
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const createInput = { title };
|
|
1860
|
+
if (input.projectId !== undefined) createInput.projectId = input.projectId;
|
|
1861
|
+
if (input.issueId !== undefined) createInput.issueId = input.issueId;
|
|
1862
|
+
|
|
1863
|
+
if (!createInput.projectId && !createInput.issueId) {
|
|
1864
|
+
throw new Error('Document create requires either projectId or issueId');
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
for (const field of ['content', 'icon', 'color']) {
|
|
1868
|
+
if (input[field] !== undefined) {
|
|
1869
|
+
createInput[field] = input[field];
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const payload = await executeGraphQL(client, DOCUMENT_CREATE_MUTATION, {
|
|
1874
|
+
input: createInput,
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
if (!payload?.documentCreate?.success || !payload?.documentCreate?.document?.id) {
|
|
1878
|
+
throw new Error('Failed to create document');
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
return fetchDocumentDetails(client, payload.documentCreate.document.id);
|
|
1882
|
+
}, 'createDocument');
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
export async function updateDocument(client, documentRef, patch = {}) {
|
|
1886
|
+
return withLinearErrorHandling(async () => {
|
|
1887
|
+
const id = String(documentRef || '').trim();
|
|
1888
|
+
if (!id) {
|
|
1889
|
+
throw new Error('Missing required field: document');
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const updateInput = {};
|
|
1893
|
+
for (const field of ['title', 'content', 'icon', 'color', 'projectId', 'issueId']) {
|
|
1894
|
+
if (patch[field] !== undefined) {
|
|
1895
|
+
updateInput[field] = patch[field];
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
if (Object.keys(updateInput).length === 0) {
|
|
1900
|
+
throw new Error('No update fields provided');
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
const payload = await executeGraphQL(client, DOCUMENT_UPDATE_MUTATION, {
|
|
1904
|
+
id,
|
|
1905
|
+
input: updateInput,
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
if (!payload?.documentUpdate?.success || !payload?.documentUpdate?.document?.id) {
|
|
1909
|
+
throw new Error('Failed to update document');
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
const document = await fetchDocumentDetails(client, payload.documentUpdate.document.id);
|
|
1913
|
+
return {
|
|
1914
|
+
document,
|
|
1915
|
+
changed: Object.keys(updateInput),
|
|
1916
|
+
};
|
|
1917
|
+
}, 'updateDocument');
|
|
1918
|
+
}
|
|
1919
|
+
|
|
919
1920
|
/**
|
|
920
1921
|
* Fetch detailed issue information including comments, parent, children, and attachments
|
|
921
1922
|
* @param {LinearClient} client - Linear SDK client
|
|
@@ -1030,6 +2031,79 @@ export async function fetchIssueDetails(client, issueRef, options = {}) {
|
|
|
1030
2031
|
}, 'fetchIssueDetails');
|
|
1031
2032
|
}
|
|
1032
2033
|
|
|
2034
|
+
export async function fetchIssueActivity(client, issueRef, options = {}) {
|
|
2035
|
+
return withLinearErrorHandling(async () => {
|
|
2036
|
+
const limit = normalizePositiveInteger(options.limit, 'limit', 20);
|
|
2037
|
+
const includeArchived = options.includeArchived === true;
|
|
2038
|
+
const resolved = await resolveIssue(client, issueRef);
|
|
2039
|
+
const data = await executeGraphQL(client, ISSUE_ACTIVITY_QUERY, {
|
|
2040
|
+
id: resolved.id,
|
|
2041
|
+
first: limit,
|
|
2042
|
+
includeArchived,
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
if (!data?.issue) {
|
|
2046
|
+
throw new Error(`Issue not found: ${issueRef}`);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
return {
|
|
2050
|
+
issue: {
|
|
2051
|
+
id: data.issue.id,
|
|
2052
|
+
identifier: data.issue.identifier,
|
|
2053
|
+
title: data.issue.title,
|
|
2054
|
+
url: data.issue.url ?? null,
|
|
2055
|
+
},
|
|
2056
|
+
activity: (data.issue.history?.nodes || []).map((entry) => ({
|
|
2057
|
+
id: entry.id,
|
|
2058
|
+
createdAt: entry.createdAt ?? null,
|
|
2059
|
+
updatedAt: entry.updatedAt ?? null,
|
|
2060
|
+
actor: entry.actor ? {
|
|
2061
|
+
id: entry.actor.id,
|
|
2062
|
+
name: entry.actor.name,
|
|
2063
|
+
displayName: entry.actor.displayName,
|
|
2064
|
+
} : null,
|
|
2065
|
+
fromState: entry.fromState ? { id: entry.fromState.id, name: entry.fromState.name } : null,
|
|
2066
|
+
toState: entry.toState ? { id: entry.toState.id, name: entry.toState.name } : null,
|
|
2067
|
+
fromAssignee: entry.fromAssignee ? {
|
|
2068
|
+
id: entry.fromAssignee.id,
|
|
2069
|
+
name: entry.fromAssignee.name,
|
|
2070
|
+
displayName: entry.fromAssignee.displayName,
|
|
2071
|
+
} : null,
|
|
2072
|
+
toAssignee: entry.toAssignee ? {
|
|
2073
|
+
id: entry.toAssignee.id,
|
|
2074
|
+
name: entry.toAssignee.name,
|
|
2075
|
+
displayName: entry.toAssignee.displayName,
|
|
2076
|
+
} : null,
|
|
2077
|
+
fromTitle: entry.fromTitle ?? null,
|
|
2078
|
+
toTitle: entry.toTitle ?? null,
|
|
2079
|
+
fromPriority: entry.fromPriority ?? null,
|
|
2080
|
+
toPriority: entry.toPriority ?? null,
|
|
2081
|
+
fromProject: entry.fromProject ? { id: entry.fromProject.id, name: entry.fromProject.name } : null,
|
|
2082
|
+
toProject: entry.toProject ? { id: entry.toProject.id, name: entry.toProject.name } : null,
|
|
2083
|
+
fromProjectMilestone: entry.fromProjectMilestone ? { id: entry.fromProjectMilestone.id, name: entry.fromProjectMilestone.name } : null,
|
|
2084
|
+
toProjectMilestone: entry.toProjectMilestone ? { id: entry.toProjectMilestone.id, name: entry.toProjectMilestone.name } : null,
|
|
2085
|
+
addedLabels: (entry.addedLabels || []).map((label) => ({ id: label.id, name: label.name })),
|
|
2086
|
+
removedLabels: (entry.removedLabels || []).map((label) => ({ id: label.id, name: label.name })),
|
|
2087
|
+
relationChanges: (entry.relationChanges || []).map((relation) => ({
|
|
2088
|
+
identifier: relation.identifier,
|
|
2089
|
+
type: relation.type,
|
|
2090
|
+
})),
|
|
2091
|
+
attachment: entry.attachment ? {
|
|
2092
|
+
id: entry.attachment.id,
|
|
2093
|
+
title: entry.attachment.title,
|
|
2094
|
+
url: entry.attachment.url,
|
|
2095
|
+
} : null,
|
|
2096
|
+
archived: entry.archived ?? null,
|
|
2097
|
+
archivedAt: entry.archivedAt ?? null,
|
|
2098
|
+
autoArchived: entry.autoArchived ?? false,
|
|
2099
|
+
autoClosed: entry.autoClosed ?? false,
|
|
2100
|
+
trashed: entry.trashed ?? null,
|
|
2101
|
+
updatedDescription: entry.updatedDescription ?? false,
|
|
2102
|
+
})),
|
|
2103
|
+
};
|
|
2104
|
+
}, 'fetchIssueActivity');
|
|
2105
|
+
}
|
|
2106
|
+
|
|
1033
2107
|
// ===== MUTATION FUNCTIONS =====
|
|
1034
2108
|
|
|
1035
2109
|
/**
|
|
@@ -1514,6 +2588,113 @@ async function transformMilestone(sdkMilestone) {
|
|
|
1514
2588
|
};
|
|
1515
2589
|
}
|
|
1516
2590
|
|
|
2591
|
+
function transformProject(rawProject) {
|
|
2592
|
+
if (!rawProject) return null;
|
|
2593
|
+
|
|
2594
|
+
const milestoneNodes = rawProject.projectMilestones?.nodes || [];
|
|
2595
|
+
const teamNodes = rawProject.teams?.nodes || [];
|
|
2596
|
+
|
|
2597
|
+
return {
|
|
2598
|
+
id: rawProject.id,
|
|
2599
|
+
name: rawProject.name,
|
|
2600
|
+
description: rawProject.description ?? '',
|
|
2601
|
+
content: rawProject.content ?? null,
|
|
2602
|
+
color: rawProject.color ?? null,
|
|
2603
|
+
icon: rawProject.icon ?? null,
|
|
2604
|
+
priority: rawProject.priority ?? null,
|
|
2605
|
+
progress: rawProject.progress ?? null,
|
|
2606
|
+
health: rawProject.health ?? null,
|
|
2607
|
+
startDate: rawProject.startDate ?? null,
|
|
2608
|
+
targetDate: rawProject.targetDate ?? null,
|
|
2609
|
+
slugId: rawProject.slugId ?? null,
|
|
2610
|
+
url: rawProject.url ?? null,
|
|
2611
|
+
archivedAt: rawProject.archivedAt ?? null,
|
|
2612
|
+
completedAt: rawProject.completedAt ?? null,
|
|
2613
|
+
canceledAt: rawProject.canceledAt ?? null,
|
|
2614
|
+
status: rawProject.status ? {
|
|
2615
|
+
id: rawProject.status.id,
|
|
2616
|
+
name: rawProject.status.name,
|
|
2617
|
+
type: rawProject.status.type,
|
|
2618
|
+
color: rawProject.status.color,
|
|
2619
|
+
} : null,
|
|
2620
|
+
lead: rawProject.lead ? {
|
|
2621
|
+
id: rawProject.lead.id,
|
|
2622
|
+
name: rawProject.lead.name,
|
|
2623
|
+
displayName: rawProject.lead.displayName,
|
|
2624
|
+
} : null,
|
|
2625
|
+
teams: teamNodes.map((team) => ({
|
|
2626
|
+
id: team.id,
|
|
2627
|
+
key: team.key,
|
|
2628
|
+
name: team.name,
|
|
2629
|
+
})),
|
|
2630
|
+
projectMilestones: milestoneNodes.map((milestone) => ({
|
|
2631
|
+
id: milestone.id,
|
|
2632
|
+
name: milestone.name,
|
|
2633
|
+
status: milestone.status,
|
|
2634
|
+
progress: milestone.progress ?? null,
|
|
2635
|
+
targetDate: milestone.targetDate ?? null,
|
|
2636
|
+
})),
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
function transformProjectUpdate(rawUpdate) {
|
|
2641
|
+
if (!rawUpdate) return null;
|
|
2642
|
+
|
|
2643
|
+
return {
|
|
2644
|
+
id: rawUpdate.id,
|
|
2645
|
+
body: rawUpdate.body ?? '',
|
|
2646
|
+
health: rawUpdate.health ?? null,
|
|
2647
|
+
createdAt: rawUpdate.createdAt ?? null,
|
|
2648
|
+
updatedAt: rawUpdate.updatedAt ?? null,
|
|
2649
|
+
archivedAt: rawUpdate.archivedAt ?? null,
|
|
2650
|
+
editedAt: rawUpdate.editedAt ?? null,
|
|
2651
|
+
url: rawUpdate.url ?? null,
|
|
2652
|
+
slugId: rawUpdate.slugId ?? null,
|
|
2653
|
+
isDiffHidden: rawUpdate.isDiffHidden ?? false,
|
|
2654
|
+
isStale: rawUpdate.isStale ?? false,
|
|
2655
|
+
project: rawUpdate.project ? {
|
|
2656
|
+
id: rawUpdate.project.id,
|
|
2657
|
+
name: rawUpdate.project.name,
|
|
2658
|
+
} : null,
|
|
2659
|
+
user: rawUpdate.user ? {
|
|
2660
|
+
id: rawUpdate.user.id,
|
|
2661
|
+
name: rawUpdate.user.name,
|
|
2662
|
+
displayName: rawUpdate.user.displayName,
|
|
2663
|
+
} : null,
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function transformDocument(rawDocument) {
|
|
2668
|
+
if (!rawDocument) return null;
|
|
2669
|
+
|
|
2670
|
+
return {
|
|
2671
|
+
id: rawDocument.id,
|
|
2672
|
+
title: rawDocument.title ?? '',
|
|
2673
|
+
content: rawDocument.content ?? '',
|
|
2674
|
+
icon: rawDocument.icon ?? null,
|
|
2675
|
+
color: rawDocument.color ?? null,
|
|
2676
|
+
slugId: rawDocument.slugId ?? null,
|
|
2677
|
+
url: rawDocument.url ?? null,
|
|
2678
|
+
archivedAt: rawDocument.archivedAt ?? null,
|
|
2679
|
+
createdAt: rawDocument.createdAt ?? null,
|
|
2680
|
+
updatedAt: rawDocument.updatedAt ?? null,
|
|
2681
|
+
project: rawDocument.project ? {
|
|
2682
|
+
id: rawDocument.project.id,
|
|
2683
|
+
name: rawDocument.project.name,
|
|
2684
|
+
} : null,
|
|
2685
|
+
issue: rawDocument.issue ? {
|
|
2686
|
+
id: rawDocument.issue.id,
|
|
2687
|
+
identifier: rawDocument.issue.identifier,
|
|
2688
|
+
title: rawDocument.issue.title,
|
|
2689
|
+
} : null,
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
function invalidateProjectsCache(client) {
|
|
2694
|
+
const cacheKey = getClientCacheKey(client);
|
|
2695
|
+
projectsCache.delete(cacheKey);
|
|
2696
|
+
}
|
|
2697
|
+
|
|
1517
2698
|
/**
|
|
1518
2699
|
* Fetch milestones for a project
|
|
1519
2700
|
* @param {LinearClient} client - Linear SDK client
|
|
@@ -2009,3 +3190,31 @@ export function formatIssueAsMarkdown(issueData, options = {}) {
|
|
|
2009
3190
|
|
|
2010
3191
|
return lines.join('\n');
|
|
2011
3192
|
}
|
|
3193
|
+
|
|
3194
|
+
export function formatIssueActivityAsMarkdown(issueData, options = {}) {
|
|
3195
|
+
const { limit } = options;
|
|
3196
|
+
const lines = [`# Activity for ${issueData.issue.identifier}: ${issueData.issue.title}`];
|
|
3197
|
+
|
|
3198
|
+
if (issueData.issue.url) {
|
|
3199
|
+
lines.push('');
|
|
3200
|
+
lines.push(`**URL:** ${issueData.issue.url}`);
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
if (!issueData.activity || issueData.activity.length === 0) {
|
|
3204
|
+
lines.push('');
|
|
3205
|
+
lines.push('No activity entries found.');
|
|
3206
|
+
return lines.join('\n');
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
lines.push('');
|
|
3210
|
+
lines.push(`Showing ${issueData.activity.length}${limit ? ` of up to ${limit}` : ''} activity entries.`);
|
|
3211
|
+
lines.push('');
|
|
3212
|
+
|
|
3213
|
+
for (const entry of issueData.activity) {
|
|
3214
|
+
const actor = entry.actor ? getUserDisplayName(entry.actor) : 'System';
|
|
3215
|
+
const timestamp = entry.createdAt ? formatRelativeTime(entry.createdAt) : 'unknown time';
|
|
3216
|
+
lines.push(`- **${actor}** ${summarizeIssueHistoryEntry(entry)} _(${timestamp})_`);
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
return lines.join('\n');
|
|
3220
|
+
}
|