@cliangdev/flux-plugin 0.3.1-dev.ee3b5ee → 0.4.0-dev.0892a21
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/package.json +1 -1
- package/src/server/adapters/github/__tests__/criteria-deps.test.ts +1 -1
- package/src/server/adapters/github/__tests__/prd-crud.test.ts +8 -8
- package/src/server/adapters/github/adapter.ts +110 -88
- package/src/server/adapters/github/client.ts +4 -3
- package/src/server/adapters/github/helpers/index-store.ts +11 -7
- package/src/server/adapters/linear/adapter.ts +121 -105
- package/src/server/adapters/linear/client.ts +21 -14
- package/src/server/tools/__tests__/z-configure-github.test.ts +22 -10
- package/src/server/tools/__tests__/z-get-linear-url.test.ts +2 -2
- package/src/server/tools/configure-github.ts +43 -9
|
@@ -21,11 +21,11 @@ async function fetchCurrentFile(
|
|
|
21
21
|
encoding: string;
|
|
22
22
|
};
|
|
23
23
|
return { sha: data.sha, content: data.content };
|
|
24
|
-
} catch (
|
|
25
|
-
if (
|
|
24
|
+
} catch (error: unknown) {
|
|
25
|
+
if ((error as { status?: number }).status === 404) {
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
|
-
throw
|
|
28
|
+
throw error;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -63,13 +63,17 @@ export async function writeIndex(
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
try {
|
|
66
|
-
await client.rest.repos.createOrUpdateFileContents(
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
await client.rest.repos.createOrUpdateFileContents(
|
|
67
|
+
params as Parameters<
|
|
68
|
+
typeof client.rest.repos.createOrUpdateFileContents
|
|
69
|
+
>[0],
|
|
70
|
+
);
|
|
71
|
+
} catch (error: unknown) {
|
|
72
|
+
if ((error as { status?: number }).status === 409) {
|
|
69
73
|
throw new Error(
|
|
70
74
|
"Index conflict: another operation modified the index concurrently. Retry the operation.",
|
|
71
75
|
);
|
|
72
76
|
}
|
|
73
|
-
throw
|
|
77
|
+
throw error;
|
|
74
78
|
}
|
|
75
79
|
}
|
|
@@ -10,6 +10,26 @@
|
|
|
10
10
|
* - Task → Linear Issue child of Epic with "task" label
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import type {
|
|
14
|
+
Attachment,
|
|
15
|
+
Issue,
|
|
16
|
+
IssueRelation,
|
|
17
|
+
WorkflowState,
|
|
18
|
+
} from "@linear/sdk";
|
|
19
|
+
import { IssueRelationType } from "@linear/sdk";
|
|
20
|
+
|
|
21
|
+
interface RawIssue {
|
|
22
|
+
update(input: Record<string, unknown>): Promise<unknown>;
|
|
23
|
+
archive(): Promise<unknown>;
|
|
24
|
+
children(): Promise<{ nodes: Issue[] }>;
|
|
25
|
+
attachments(): Promise<{ nodes: ArchivableAttachment[] }>;
|
|
26
|
+
inverseRelations(): Promise<{ nodes: IssueRelation[] }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ArchivableAttachment extends Attachment {
|
|
30
|
+
archive(): Promise<unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
import type {
|
|
14
34
|
AcceptanceCriterion,
|
|
15
35
|
AddCriterionInput,
|
|
@@ -70,8 +90,7 @@ export interface HydratedIssue {
|
|
|
70
90
|
priority: number;
|
|
71
91
|
createdAt: Date;
|
|
72
92
|
updatedAt: Date;
|
|
73
|
-
|
|
74
|
-
_raw: any;
|
|
93
|
+
_raw: unknown;
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
export class LinearAdapter implements BackendAdapter {
|
|
@@ -87,6 +106,10 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
87
106
|
// Helper Methods
|
|
88
107
|
// -------------------------------------------------------------------------
|
|
89
108
|
|
|
109
|
+
private raw(issue: HydratedIssue): RawIssue {
|
|
110
|
+
return issue._raw as RawIssue;
|
|
111
|
+
}
|
|
112
|
+
|
|
90
113
|
private extractIssueNumber(identifier: string): number {
|
|
91
114
|
const match = identifier.match(/-(\d+)$/);
|
|
92
115
|
if (!match) {
|
|
@@ -98,12 +121,16 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
98
121
|
/**
|
|
99
122
|
* Hydrate a raw Linear issue by resolving all lazy-loaded fields in parallel.
|
|
100
123
|
*/
|
|
101
|
-
async hydrateIssue(issue:
|
|
124
|
+
async hydrateIssue(issue: Issue): Promise<HydratedIssue> {
|
|
125
|
+
const statePromise = issue.state;
|
|
126
|
+
const parentPromise = issue.parent;
|
|
102
127
|
const [state, labelsResult, parent] = await Promise.all([
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
statePromise
|
|
129
|
+
? this.client.execute<WorkflowState>(() => statePromise)
|
|
130
|
+
: Promise.resolve(undefined),
|
|
131
|
+
this.client.execute(() => issue.labels()),
|
|
132
|
+
parentPromise
|
|
133
|
+
? this.client.execute<Issue>(() => parentPromise)
|
|
107
134
|
: Promise.resolve(null),
|
|
108
135
|
]);
|
|
109
136
|
|
|
@@ -111,10 +138,10 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
111
138
|
id: issue.id,
|
|
112
139
|
identifier: issue.identifier,
|
|
113
140
|
title: issue.title,
|
|
114
|
-
description: issue.description,
|
|
141
|
+
description: issue.description ?? undefined,
|
|
115
142
|
stateName: state?.name || "Backlog",
|
|
116
143
|
stateType: state?.type,
|
|
117
|
-
labels: (labelsResult?.nodes || []).map((l
|
|
144
|
+
labels: (labelsResult?.nodes || []).map((l) => l.name),
|
|
118
145
|
parentIdentifier: parent?.identifier,
|
|
119
146
|
priority: issue.priority ?? 3,
|
|
120
147
|
createdAt: issue.createdAt,
|
|
@@ -146,14 +173,15 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
146
173
|
/**
|
|
147
174
|
* Fetch multiple issues with a filter and hydrate them.
|
|
148
175
|
*/
|
|
149
|
-
async fetchIssues(
|
|
176
|
+
async fetchIssues(
|
|
177
|
+
filter: Record<string, unknown>,
|
|
178
|
+
limit: number,
|
|
179
|
+
): Promise<HydratedIssue[]> {
|
|
150
180
|
const result = await this.client.execute(() =>
|
|
151
181
|
this.client.client.issues({ filter, first: limit }),
|
|
152
182
|
);
|
|
153
183
|
|
|
154
|
-
return Promise.all(
|
|
155
|
-
result.nodes.map((issue: any) => this.hydrateIssue(issue)),
|
|
156
|
-
);
|
|
184
|
+
return Promise.all(result.nodes.map((issue) => this.hydrateIssue(issue)));
|
|
157
185
|
}
|
|
158
186
|
|
|
159
187
|
/**
|
|
@@ -185,7 +213,7 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
185
213
|
this.client.client.team(this.config.teamId),
|
|
186
214
|
);
|
|
187
215
|
const states = await this.client.execute(() => team.states());
|
|
188
|
-
const targetState = states.nodes.find((s
|
|
216
|
+
const targetState = states.nodes.find((s) => s.name === stateName);
|
|
189
217
|
if (!targetState) {
|
|
190
218
|
throw new Error(`Workflow state '${stateName}' not found`);
|
|
191
219
|
}
|
|
@@ -274,8 +302,10 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
274
302
|
}),
|
|
275
303
|
);
|
|
276
304
|
|
|
277
|
-
const rawIssue = await this.client.execute<
|
|
278
|
-
() =>
|
|
305
|
+
const rawIssue = await this.client.execute<Issue | undefined>(
|
|
306
|
+
() =>
|
|
307
|
+
(createResult as unknown as { issue: Promise<Issue | undefined> })
|
|
308
|
+
.issue,
|
|
279
309
|
);
|
|
280
310
|
if (!rawIssue) {
|
|
281
311
|
throw new Error("Failed to create PRD issue");
|
|
@@ -290,7 +320,7 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
290
320
|
if (!issue) throw new Error(`PRD not found: ${ref}`);
|
|
291
321
|
if (!this.isPrd(issue)) throw new Error(`Issue ${ref} is not a PRD`);
|
|
292
322
|
|
|
293
|
-
const updatePayload:
|
|
323
|
+
const updatePayload: Record<string, unknown> = {};
|
|
294
324
|
if (input.title !== undefined) updatePayload.title = input.title;
|
|
295
325
|
if (input.description !== undefined)
|
|
296
326
|
updatePayload.description = input.description;
|
|
@@ -317,38 +347,13 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
317
347
|
);
|
|
318
348
|
}
|
|
319
349
|
|
|
320
|
-
await this.client.execute(() => issue.
|
|
350
|
+
await this.client.execute(() => this.raw(issue).update(updatePayload));
|
|
321
351
|
|
|
322
352
|
const updated = await this.fetchIssue(ref);
|
|
323
353
|
if (!updated) throw new Error(`Failed to fetch updated PRD: ${ref}`);
|
|
324
354
|
return this.toPrd(updated);
|
|
325
355
|
}
|
|
326
356
|
|
|
327
|
-
/**
|
|
328
|
-
* Build label IDs for a PRD status update.
|
|
329
|
-
* Keeps existing non-status labels, adds new status label if needed.
|
|
330
|
-
* @deprecated Use buildPrdLabelIdsWithTag for updates that may include tag changes
|
|
331
|
-
*/
|
|
332
|
-
private async buildPrdLabelIds(
|
|
333
|
-
currentLabels: string[],
|
|
334
|
-
newStatus: import("../types.js").PrdStatus,
|
|
335
|
-
): Promise<string[]> {
|
|
336
|
-
const statusLabels = getAllStatusLabels();
|
|
337
|
-
const newStatusLabel = getStatusLabelForPrdStatus(newStatus);
|
|
338
|
-
|
|
339
|
-
const labelsToKeep = currentLabels.filter((l) => !statusLabels.includes(l));
|
|
340
|
-
if (newStatusLabel) {
|
|
341
|
-
labelsToKeep.push(newStatusLabel);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const labelIds: string[] = [];
|
|
345
|
-
for (const labelName of labelsToKeep) {
|
|
346
|
-
const id = await this.getOrCreateLabel(labelName);
|
|
347
|
-
labelIds.push(id);
|
|
348
|
-
}
|
|
349
|
-
return labelIds;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
357
|
/**
|
|
353
358
|
* Build label IDs for a PRD update, handling both status and tag changes.
|
|
354
359
|
* Filters out status labels and milestone labels, then adds the appropriate ones back.
|
|
@@ -393,14 +398,20 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
393
398
|
try {
|
|
394
399
|
return await this.getLabelId(labelName);
|
|
395
400
|
} catch {
|
|
396
|
-
const result = await this.client.execute
|
|
397
|
-
|
|
401
|
+
const result = await this.client.execute(() =>
|
|
402
|
+
this.client.client.createIssueLabel({
|
|
398
403
|
name: labelName,
|
|
399
404
|
teamId: this.config.teamId,
|
|
400
405
|
}),
|
|
401
406
|
);
|
|
402
|
-
|
|
403
|
-
const
|
|
407
|
+
const labelPayload = result as unknown as Record<string, unknown>;
|
|
408
|
+
const issueLabelField = labelPayload._issueLabel as
|
|
409
|
+
| { id: string }
|
|
410
|
+
| undefined;
|
|
411
|
+
const issueLabelGetter = labelPayload.issueLabel as
|
|
412
|
+
| { id: string }
|
|
413
|
+
| undefined;
|
|
414
|
+
const labelId = issueLabelField?.id ?? issueLabelGetter?.id;
|
|
404
415
|
if (!labelId || typeof labelId !== "string") {
|
|
405
416
|
throw new Error(
|
|
406
417
|
`Failed to create label: ${labelName}. Result: ${JSON.stringify(result)}`,
|
|
@@ -423,11 +434,10 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
423
434
|
const limit = pagination?.limit ?? 50;
|
|
424
435
|
const offset = pagination?.offset ?? 0;
|
|
425
436
|
|
|
426
|
-
const linearFilter:
|
|
437
|
+
const linearFilter: Record<string, unknown> = {
|
|
427
438
|
project: { id: { eq: this.config.projectId } },
|
|
428
439
|
};
|
|
429
440
|
|
|
430
|
-
// Build label filter - always include prd label, optionally include tag label
|
|
431
441
|
if (filters?.tag) {
|
|
432
442
|
linearFilter.labels = {
|
|
433
443
|
and: [
|
|
@@ -464,8 +474,8 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
464
474
|
const issue = await this.fetchIssue(ref);
|
|
465
475
|
if (!issue) throw new Error(`PRD not found: ${ref}`);
|
|
466
476
|
|
|
467
|
-
const childrenResult = await this.client.execute
|
|
468
|
-
issue.
|
|
477
|
+
const childrenResult = await this.client.execute(() =>
|
|
478
|
+
this.raw(issue).children(),
|
|
469
479
|
);
|
|
470
480
|
const children = childrenResult.nodes || [];
|
|
471
481
|
|
|
@@ -475,19 +485,17 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
475
485
|
for (const child of children) {
|
|
476
486
|
const hydratedChild = await this.hydrateIssue(child);
|
|
477
487
|
if (this.isEpic(hydratedChild)) {
|
|
478
|
-
const epicChildren = await this.client.execute
|
|
479
|
-
child.children(),
|
|
480
|
-
);
|
|
488
|
+
const epicChildren = await this.client.execute(() => child.children());
|
|
481
489
|
for (const task of epicChildren.nodes || []) {
|
|
482
|
-
await this.client.execute
|
|
490
|
+
await this.client.execute(() => task.archive());
|
|
483
491
|
taskCount++;
|
|
484
492
|
}
|
|
485
|
-
await this.client.execute
|
|
493
|
+
await this.client.execute(() => child.archive());
|
|
486
494
|
epicCount++;
|
|
487
495
|
}
|
|
488
496
|
}
|
|
489
497
|
|
|
490
|
-
await this.client.execute
|
|
498
|
+
await this.client.execute(() => this.raw(issue).archive());
|
|
491
499
|
|
|
492
500
|
return {
|
|
493
501
|
deleted: ref,
|
|
@@ -529,8 +537,10 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
529
537
|
}),
|
|
530
538
|
);
|
|
531
539
|
|
|
532
|
-
const rawIssue = await this.client.execute<
|
|
533
|
-
() =>
|
|
540
|
+
const rawIssue = await this.client.execute<Issue | undefined>(
|
|
541
|
+
() =>
|
|
542
|
+
(createResult as unknown as { issue: Promise<Issue | undefined> })
|
|
543
|
+
.issue,
|
|
534
544
|
);
|
|
535
545
|
if (!rawIssue) throw new Error("Failed to create Epic issue");
|
|
536
546
|
|
|
@@ -542,7 +552,7 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
542
552
|
const issue = await this.fetchIssue(ref);
|
|
543
553
|
if (!issue) throw new Error(`Epic not found: ${ref}`);
|
|
544
554
|
|
|
545
|
-
const updatePayload:
|
|
555
|
+
const updatePayload: Record<string, unknown> = {};
|
|
546
556
|
if (input.title !== undefined) updatePayload.title = input.title;
|
|
547
557
|
if (input.description !== undefined)
|
|
548
558
|
updatePayload.description = input.description;
|
|
@@ -552,8 +562,7 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
552
562
|
);
|
|
553
563
|
}
|
|
554
564
|
|
|
555
|
-
await this.client.execute(() => issue.
|
|
556
|
-
// Re-fetch to get the updated issue with all fields
|
|
565
|
+
await this.client.execute(() => this.raw(issue).update(updatePayload));
|
|
557
566
|
const updated = await this.fetchIssue(ref);
|
|
558
567
|
if (!updated) {
|
|
559
568
|
throw new Error(`Failed to fetch updated Epic: ${ref}`);
|
|
@@ -574,7 +583,7 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
574
583
|
const limit = pagination?.limit ?? 50;
|
|
575
584
|
const offset = pagination?.offset ?? 0;
|
|
576
585
|
|
|
577
|
-
const linearFilter:
|
|
586
|
+
const linearFilter: Record<string, unknown> = {
|
|
578
587
|
labels: { name: { eq: this.config.defaultLabels.epic } },
|
|
579
588
|
project: { id: { eq: this.config.projectId } },
|
|
580
589
|
};
|
|
@@ -609,16 +618,16 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
609
618
|
const issue = await this.fetchIssue(ref);
|
|
610
619
|
if (!issue) throw new Error(`Epic not found: ${ref}`);
|
|
611
620
|
|
|
612
|
-
const childrenResult = await this.client.execute
|
|
613
|
-
issue.
|
|
621
|
+
const childrenResult = await this.client.execute(() =>
|
|
622
|
+
this.raw(issue).children(),
|
|
614
623
|
);
|
|
615
624
|
const children = childrenResult.nodes || [];
|
|
616
625
|
|
|
617
626
|
for (const child of children) {
|
|
618
|
-
await this.client.execute
|
|
627
|
+
await this.client.execute(() => child.archive());
|
|
619
628
|
}
|
|
620
629
|
|
|
621
|
-
await this.client.execute
|
|
630
|
+
await this.client.execute(() => this.raw(issue).archive());
|
|
622
631
|
|
|
623
632
|
return {
|
|
624
633
|
deleted: ref,
|
|
@@ -659,8 +668,10 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
659
668
|
}),
|
|
660
669
|
);
|
|
661
670
|
|
|
662
|
-
const rawIssue = await this.client.execute<
|
|
663
|
-
() =>
|
|
671
|
+
const rawIssue = await this.client.execute<Issue | undefined>(
|
|
672
|
+
() =>
|
|
673
|
+
(createResult as unknown as { issue: Promise<Issue | undefined> })
|
|
674
|
+
.issue,
|
|
664
675
|
);
|
|
665
676
|
if (!rawIssue) throw new Error("Failed to create Task issue");
|
|
666
677
|
|
|
@@ -672,7 +683,7 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
672
683
|
const issue = await this.fetchIssue(ref);
|
|
673
684
|
if (!issue) throw new Error(`Task not found: ${ref}`);
|
|
674
685
|
|
|
675
|
-
const updatePayload:
|
|
686
|
+
const updatePayload: Record<string, unknown> = {};
|
|
676
687
|
if (input.title !== undefined) updatePayload.title = input.title;
|
|
677
688
|
if (input.description !== undefined)
|
|
678
689
|
updatePayload.description = input.description;
|
|
@@ -684,11 +695,11 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
684
695
|
this.client.client.team(this.config.teamId),
|
|
685
696
|
);
|
|
686
697
|
const states = await this.client.execute(() => team.states());
|
|
687
|
-
const targetState = states.nodes.find((s
|
|
698
|
+
const targetState = states.nodes.find((s) => s.name === stateName);
|
|
688
699
|
if (targetState) updatePayload.stateId = targetState.id;
|
|
689
700
|
}
|
|
690
701
|
|
|
691
|
-
await this.client.execute(() => issue.
|
|
702
|
+
await this.client.execute(() => this.raw(issue).update(updatePayload));
|
|
692
703
|
// Re-fetch to get the updated issue with all fields
|
|
693
704
|
const updated = await this.fetchIssue(ref);
|
|
694
705
|
if (!updated) {
|
|
@@ -712,7 +723,7 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
712
723
|
const limit = pagination?.limit ?? 50;
|
|
713
724
|
const offset = pagination?.offset ?? 0;
|
|
714
725
|
|
|
715
|
-
const linearFilter:
|
|
726
|
+
const linearFilter: Record<string, unknown> = {
|
|
716
727
|
labels: { name: { eq: this.config.defaultLabels.task } },
|
|
717
728
|
project: { id: { eq: this.config.projectId } },
|
|
718
729
|
};
|
|
@@ -731,7 +742,7 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
731
742
|
);
|
|
732
743
|
|
|
733
744
|
const issues = await Promise.all(
|
|
734
|
-
result.nodes.map((i
|
|
745
|
+
result.nodes.map((i) => this.hydrateIssue(i)),
|
|
735
746
|
);
|
|
736
747
|
const paginated = issues.slice(offset, offset + limit);
|
|
737
748
|
|
|
@@ -750,7 +761,7 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
750
761
|
const issue = await this.fetchIssue(ref);
|
|
751
762
|
if (!issue) throw new Error(`Task not found: ${ref}`);
|
|
752
763
|
|
|
753
|
-
await this.client.execute(() => issue.
|
|
764
|
+
await this.client.execute(() => this.raw(issue).archive());
|
|
754
765
|
|
|
755
766
|
return {
|
|
756
767
|
deleted: ref,
|
|
@@ -774,8 +785,8 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
774
785
|
input.criteria,
|
|
775
786
|
);
|
|
776
787
|
|
|
777
|
-
await this.client.execute
|
|
778
|
-
issue.
|
|
788
|
+
await this.client.execute(() =>
|
|
789
|
+
this.raw(issue).update({ description: newDescription }),
|
|
779
790
|
);
|
|
780
791
|
|
|
781
792
|
return {
|
|
@@ -808,8 +819,8 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
808
819
|
true,
|
|
809
820
|
);
|
|
810
821
|
|
|
811
|
-
await this.client.execute
|
|
812
|
-
issue.
|
|
822
|
+
await this.client.execute(() =>
|
|
823
|
+
this.raw(issue).update({ description: newDescription }),
|
|
813
824
|
);
|
|
814
825
|
|
|
815
826
|
return {
|
|
@@ -869,11 +880,11 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
869
880
|
);
|
|
870
881
|
}
|
|
871
882
|
|
|
872
|
-
await this.client.execute
|
|
883
|
+
await this.client.execute(() =>
|
|
873
884
|
this.client.client.createIssueRelation({
|
|
874
885
|
issueId: blockerIssue.id,
|
|
875
886
|
relatedIssueId: blockedIssue.id,
|
|
876
|
-
type:
|
|
887
|
+
type: IssueRelationType.Blocks,
|
|
877
888
|
}),
|
|
878
889
|
);
|
|
879
890
|
}
|
|
@@ -887,13 +898,11 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
887
898
|
if (!blockedIssue) throw new Error(`Issue not found: ${ref}`);
|
|
888
899
|
if (!blockerIssue) throw new Error(`Issue not found: ${dependsOnRef}`);
|
|
889
900
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
blockedIssue._raw.inverseRelations(),
|
|
901
|
+
const relations = await this.client.execute(() =>
|
|
902
|
+
this.raw(blockedIssue).inverseRelations(),
|
|
893
903
|
);
|
|
894
904
|
|
|
895
|
-
|
|
896
|
-
let relationToDelete: any = null;
|
|
905
|
+
let relationToDelete: IssueRelation | null = null;
|
|
897
906
|
for (const rel of relations.nodes) {
|
|
898
907
|
if (rel.type === "blocks") {
|
|
899
908
|
const relIssue = await rel.issue;
|
|
@@ -910,17 +919,15 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
910
919
|
);
|
|
911
920
|
}
|
|
912
921
|
|
|
913
|
-
await this.client.execute
|
|
922
|
+
await this.client.execute(() => relationToDelete.delete());
|
|
914
923
|
}
|
|
915
924
|
|
|
916
925
|
async getDependencies(ref: string): Promise<string[]> {
|
|
917
926
|
const issue = await this.fetchIssue(ref);
|
|
918
927
|
if (!issue) throw new Error(`Issue not found: ${ref}`);
|
|
919
928
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const relations = await this.client.execute<any>(() =>
|
|
923
|
-
issue._raw.inverseRelations(),
|
|
929
|
+
const relations = await this.client.execute(() =>
|
|
930
|
+
this.raw(issue).inverseRelations(),
|
|
924
931
|
);
|
|
925
932
|
|
|
926
933
|
const blockingRefs: string[] = [];
|
|
@@ -952,21 +959,32 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
952
959
|
const issue = await this.fetchIssue(doc.prdRef);
|
|
953
960
|
if (!issue) throw new Error(`PRD ${doc.prdRef} not found`);
|
|
954
961
|
|
|
955
|
-
const existingAttachments = await this.client.execute
|
|
956
|
-
issue.
|
|
962
|
+
const existingAttachments = await this.client.execute(() =>
|
|
963
|
+
this.raw(issue).attachments(),
|
|
957
964
|
);
|
|
958
965
|
const existingAttachment = existingAttachments.nodes.find(
|
|
959
|
-
(att
|
|
966
|
+
(att) => att.title === doc.filename,
|
|
960
967
|
);
|
|
961
968
|
|
|
962
969
|
if (existingAttachment) {
|
|
963
|
-
await this.client.execute
|
|
970
|
+
await this.client.execute(() => existingAttachment.archive());
|
|
964
971
|
}
|
|
965
972
|
|
|
966
973
|
const contentUrl = `data:text/markdown;base64,${Buffer.from(doc.content).toString("base64")}`;
|
|
967
974
|
|
|
968
|
-
const attachResult = await this.client.execute<
|
|
969
|
-
|
|
975
|
+
const attachResult = await this.client.execute<{
|
|
976
|
+
success: boolean;
|
|
977
|
+
attachment?: { url: string };
|
|
978
|
+
}>(() =>
|
|
979
|
+
(
|
|
980
|
+
this.client.client as unknown as {
|
|
981
|
+
attachmentCreate(input: {
|
|
982
|
+
title: string;
|
|
983
|
+
url: string;
|
|
984
|
+
issueId: string;
|
|
985
|
+
}): Promise<{ success: boolean; attachment?: { url: string } }>;
|
|
986
|
+
}
|
|
987
|
+
).attachmentCreate({
|
|
970
988
|
title: doc.filename,
|
|
971
989
|
url: contentUrl,
|
|
972
990
|
issueId: issue.id,
|
|
@@ -989,8 +1007,8 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
989
1007
|
const issue = await this.fetchIssue(prdRef);
|
|
990
1008
|
if (!issue) throw new Error(`PRD ${prdRef} not found`);
|
|
991
1009
|
|
|
992
|
-
const attachments = await this.client.execute
|
|
993
|
-
issue.
|
|
1010
|
+
const attachments = await this.client.execute(() =>
|
|
1011
|
+
this.raw(issue).attachments(),
|
|
994
1012
|
);
|
|
995
1013
|
const documents: Document[] = [];
|
|
996
1014
|
|
|
@@ -1021,16 +1039,14 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
1021
1039
|
const issue = await this.fetchIssue(prdRef);
|
|
1022
1040
|
if (!issue) throw new Error(`PRD ${prdRef} not found`);
|
|
1023
1041
|
|
|
1024
|
-
const attachments = await this.client.execute
|
|
1025
|
-
issue.
|
|
1026
|
-
);
|
|
1027
|
-
const attachment = attachments.nodes.find(
|
|
1028
|
-
(att: any) => att.title === filename,
|
|
1042
|
+
const attachments = await this.client.execute(() =>
|
|
1043
|
+
this.raw(issue).attachments(),
|
|
1029
1044
|
);
|
|
1045
|
+
const attachment = attachments.nodes.find((att) => att.title === filename);
|
|
1030
1046
|
|
|
1031
1047
|
if (!attachment) throw new Error(`Document ${filename} not found`);
|
|
1032
1048
|
|
|
1033
|
-
await this.client.execute
|
|
1049
|
+
await this.client.execute(() => attachment.archive());
|
|
1034
1050
|
}
|
|
1035
1051
|
|
|
1036
1052
|
// -------------------------------------------------------------------------
|
|
@@ -73,13 +73,20 @@ export class LinearClient {
|
|
|
73
73
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
74
74
|
try {
|
|
75
75
|
return await operation();
|
|
76
|
-
} catch (error:
|
|
77
|
-
|
|
76
|
+
} catch (error: unknown) {
|
|
77
|
+
const err = error as {
|
|
78
|
+
status?: number;
|
|
79
|
+
message?: string;
|
|
80
|
+
code?: string;
|
|
81
|
+
};
|
|
82
|
+
const asError =
|
|
83
|
+
error instanceof Error ? error : new Error(String(error));
|
|
84
|
+
lastError = asError;
|
|
78
85
|
|
|
79
86
|
// Check if error is retryable
|
|
80
|
-
const isRateLimited =
|
|
81
|
-
const isNetworkError = this.isNetworkError(
|
|
82
|
-
const isUnauthorized =
|
|
87
|
+
const isRateLimited = err.status === 429;
|
|
88
|
+
const isNetworkError = this.isNetworkError(err);
|
|
89
|
+
const isUnauthorized = err.status === 401;
|
|
83
90
|
|
|
84
91
|
// Throw immediately for unauthorized errors
|
|
85
92
|
if (isUnauthorized) {
|
|
@@ -87,17 +94,17 @@ export class LinearClient {
|
|
|
87
94
|
"Unauthorized: Invalid API key",
|
|
88
95
|
"UNAUTHORIZED",
|
|
89
96
|
401,
|
|
90
|
-
|
|
97
|
+
asError,
|
|
91
98
|
);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
// Throw immediately for non-retryable errors
|
|
95
102
|
if (!isRateLimited && !isNetworkError) {
|
|
96
103
|
throw new LinearApiError(
|
|
97
|
-
|
|
104
|
+
err.message || "Linear API error",
|
|
98
105
|
"API_ERROR",
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
err.status,
|
|
107
|
+
asError,
|
|
101
108
|
);
|
|
102
109
|
}
|
|
103
110
|
|
|
@@ -108,14 +115,14 @@ export class LinearClient {
|
|
|
108
115
|
`Rate limited after ${this.maxRetries} retries`,
|
|
109
116
|
"RATE_LIMITED",
|
|
110
117
|
429,
|
|
111
|
-
|
|
118
|
+
asError,
|
|
112
119
|
);
|
|
113
120
|
}
|
|
114
121
|
throw new LinearApiError(
|
|
115
|
-
`Network error after ${this.maxRetries} retries: ${
|
|
122
|
+
`Network error after ${this.maxRetries} retries: ${asError.message}`,
|
|
116
123
|
"NETWORK_ERROR",
|
|
117
124
|
undefined,
|
|
118
|
-
|
|
125
|
+
asError,
|
|
119
126
|
);
|
|
120
127
|
}
|
|
121
128
|
|
|
@@ -137,7 +144,7 @@ export class LinearClient {
|
|
|
137
144
|
/**
|
|
138
145
|
* Check if an error is a network error.
|
|
139
146
|
*/
|
|
140
|
-
private isNetworkError(error:
|
|
147
|
+
private isNetworkError(error: { code?: string; message?: string }): boolean {
|
|
141
148
|
// Common network error codes
|
|
142
149
|
const networkErrorCodes = [
|
|
143
150
|
"ECONNRESET",
|
|
@@ -147,7 +154,7 @@ export class LinearClient {
|
|
|
147
154
|
"ENETUNREACH",
|
|
148
155
|
];
|
|
149
156
|
|
|
150
|
-
return networkErrorCodes.includes(error.code);
|
|
157
|
+
return error.code !== undefined && networkErrorCodes.includes(error.code);
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
/**
|