@stoneforge/quarry 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli/commands/auto-link-helper.d.ts +33 -0
- package/dist/cli/commands/auto-link-helper.d.ts.map +1 -0
- package/dist/cli/commands/auto-link-helper.js +73 -0
- package/dist/cli/commands/auto-link-helper.js.map +1 -0
- package/dist/cli/commands/crud.d.ts +1 -0
- package/dist/cli/commands/crud.d.ts.map +1 -1
- package/dist/cli/commands/crud.js +44 -5
- package/dist/cli/commands/crud.js.map +1 -1
- package/dist/cli/commands/external-sync.d.ts +17 -0
- package/dist/cli/commands/external-sync.d.ts.map +1 -0
- package/dist/cli/commands/external-sync.js +1647 -0
- package/dist/cli/commands/external-sync.js.map +1 -0
- package/dist/cli/runner.d.ts.map +1 -1
- package/dist/cli/runner.js +3 -0
- package/dist/cli/runner.js.map +1 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +28 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/defaults.d.ts +13 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +21 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/file.d.ts.map +1 -1
- package/dist/config/file.js +61 -0
- package/dist/config/file.js.map +1 -1
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/config/merge.d.ts.map +1 -1
- package/dist/config/merge.js +46 -1
- package/dist/config/merge.js.map +1 -1
- package/dist/config/types.d.ts +63 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +30 -0
- package/dist/config/types.js.map +1 -1
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +51 -1
- package/dist/config/validation.js.map +1 -1
- package/dist/external-sync/adapters/task-sync-adapter.d.ts +177 -0
- package/dist/external-sync/adapters/task-sync-adapter.d.ts.map +1 -0
- package/dist/external-sync/adapters/task-sync-adapter.js +353 -0
- package/dist/external-sync/adapters/task-sync-adapter.js.map +1 -0
- package/dist/external-sync/auto-link.d.ts +66 -0
- package/dist/external-sync/auto-link.d.ts.map +1 -0
- package/dist/external-sync/auto-link.js +98 -0
- package/dist/external-sync/auto-link.js.map +1 -0
- package/dist/external-sync/conflict-resolver.d.ts +170 -0
- package/dist/external-sync/conflict-resolver.d.ts.map +1 -0
- package/dist/external-sync/conflict-resolver.js +580 -0
- package/dist/external-sync/conflict-resolver.js.map +1 -0
- package/dist/external-sync/index.d.ts +20 -0
- package/dist/external-sync/index.d.ts.map +1 -0
- package/dist/external-sync/index.js +20 -0
- package/dist/external-sync/index.js.map +1 -0
- package/dist/external-sync/provider-registry.d.ts +109 -0
- package/dist/external-sync/provider-registry.d.ts.map +1 -0
- package/dist/external-sync/provider-registry.js +188 -0
- package/dist/external-sync/provider-registry.js.map +1 -0
- package/dist/external-sync/providers/github/github-api.d.ts +271 -0
- package/dist/external-sync/providers/github/github-api.d.ts.map +1 -0
- package/dist/external-sync/providers/github/github-api.js +366 -0
- package/dist/external-sync/providers/github/github-api.js.map +1 -0
- package/dist/external-sync/providers/github/github-field-map.d.ts +76 -0
- package/dist/external-sync/providers/github/github-field-map.d.ts.map +1 -0
- package/dist/external-sync/providers/github/github-field-map.js +157 -0
- package/dist/external-sync/providers/github/github-field-map.js.map +1 -0
- package/dist/external-sync/providers/github/github-provider.d.ts +36 -0
- package/dist/external-sync/providers/github/github-provider.d.ts.map +1 -0
- package/dist/external-sync/providers/github/github-provider.js +212 -0
- package/dist/external-sync/providers/github/github-provider.js.map +1 -0
- package/dist/external-sync/providers/github/github-task-adapter.d.ts +135 -0
- package/dist/external-sync/providers/github/github-task-adapter.d.ts.map +1 -0
- package/dist/external-sync/providers/github/github-task-adapter.js +374 -0
- package/dist/external-sync/providers/github/github-task-adapter.js.map +1 -0
- package/dist/external-sync/providers/github/index.d.ts +12 -0
- package/dist/external-sync/providers/github/index.d.ts.map +1 -0
- package/dist/external-sync/providers/github/index.js +15 -0
- package/dist/external-sync/providers/github/index.js.map +1 -0
- package/dist/external-sync/providers/index.d.ts +9 -0
- package/dist/external-sync/providers/index.d.ts.map +1 -0
- package/dist/external-sync/providers/index.js +10 -0
- package/dist/external-sync/providers/index.js.map +1 -0
- package/dist/external-sync/providers/linear/index.d.ts +19 -0
- package/dist/external-sync/providers/linear/index.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/index.js +19 -0
- package/dist/external-sync/providers/linear/index.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-api.d.ts +252 -0
- package/dist/external-sync/providers/linear/linear-api.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-api.js +522 -0
- package/dist/external-sync/providers/linear/linear-api.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-field-map.d.ts +135 -0
- package/dist/external-sync/providers/linear/linear-field-map.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-field-map.js +338 -0
- package/dist/external-sync/providers/linear/linear-field-map.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-provider.d.ts +52 -0
- package/dist/external-sync/providers/linear/linear-provider.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-provider.js +169 -0
- package/dist/external-sync/providers/linear/linear-provider.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-task-adapter.d.ts +190 -0
- package/dist/external-sync/providers/linear/linear-task-adapter.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-task-adapter.js +521 -0
- package/dist/external-sync/providers/linear/linear-task-adapter.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-types.d.ts +114 -0
- package/dist/external-sync/providers/linear/linear-types.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-types.js +10 -0
- package/dist/external-sync/providers/linear/linear-types.js.map +1 -0
- package/dist/external-sync/sync-engine.d.ts +298 -0
- package/dist/external-sync/sync-engine.d.ts.map +1 -0
- package/dist/external-sync/sync-engine.js +785 -0
- package/dist/external-sync/sync-engine.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/services/inbox.js +1 -1
- package/dist/sync/hash.d.ts +5 -0
- package/dist/sync/hash.d.ts.map +1 -1
- package/dist/sync/hash.js +21 -2
- package/dist/sync/hash.js.map +1 -1
- package/package.json +11 -5
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear GraphQL API Client
|
|
3
|
+
*
|
|
4
|
+
* Pure fetch-based client for Linear issue operations via GraphQL.
|
|
5
|
+
* Supports API key authentication, rate limit handling, cursor pagination,
|
|
6
|
+
* and typed error responses.
|
|
7
|
+
*
|
|
8
|
+
* No external dependencies — uses only the standard fetch API.
|
|
9
|
+
*
|
|
10
|
+
* @see https://developers.linear.app/docs/graphql/working-with-the-graphql-api
|
|
11
|
+
*/
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Error Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Typed error for Linear API failures.
|
|
17
|
+
* Wraps fetch errors with status code, GraphQL errors, and rate limit info.
|
|
18
|
+
*/
|
|
19
|
+
export class LinearApiError extends Error {
|
|
20
|
+
/** HTTP status code from Linear's response */
|
|
21
|
+
status;
|
|
22
|
+
/** GraphQL errors from the response (if any) */
|
|
23
|
+
graphqlErrors;
|
|
24
|
+
/** Rate limit information at the time of the error */
|
|
25
|
+
rateLimit;
|
|
26
|
+
constructor(message, status, graphqlErrors = [], rateLimit = null, cause) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'LinearApiError';
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.graphqlErrors = graphqlErrors;
|
|
31
|
+
this.rateLimit = rateLimit;
|
|
32
|
+
this.cause = cause;
|
|
33
|
+
if (Error.captureStackTrace) {
|
|
34
|
+
Error.captureStackTrace(this, LinearApiError);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether this error is due to rate limiting.
|
|
39
|
+
*/
|
|
40
|
+
get isRateLimited() {
|
|
41
|
+
return this.status === 429 || (this.rateLimit !== null && this.rateLimit.remaining === 0);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether this error is due to authentication failure.
|
|
45
|
+
*/
|
|
46
|
+
get isAuthError() {
|
|
47
|
+
return this.status === 401;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns a JSON-serializable representation of the error.
|
|
51
|
+
*/
|
|
52
|
+
toJSON() {
|
|
53
|
+
return {
|
|
54
|
+
name: this.name,
|
|
55
|
+
message: this.message,
|
|
56
|
+
status: this.status,
|
|
57
|
+
graphqlErrors: this.graphqlErrors,
|
|
58
|
+
rateLimit: this.rateLimit,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Type guard for LinearApiError.
|
|
64
|
+
*/
|
|
65
|
+
export function isLinearApiError(error) {
|
|
66
|
+
return error instanceof LinearApiError;
|
|
67
|
+
}
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Constants
|
|
70
|
+
// ============================================================================
|
|
71
|
+
/** Linear GraphQL API endpoint */
|
|
72
|
+
const LINEAR_API_URL = 'https://api.linear.app/graphql';
|
|
73
|
+
/** Default threshold of remaining requests before logging a warning */
|
|
74
|
+
const DEFAULT_RATE_LIMIT_WARNING_THRESHOLD = 100;
|
|
75
|
+
/** Common issue fields fragment for GraphQL queries */
|
|
76
|
+
const ISSUE_FIELDS = `
|
|
77
|
+
id
|
|
78
|
+
identifier
|
|
79
|
+
title
|
|
80
|
+
description
|
|
81
|
+
priority
|
|
82
|
+
url
|
|
83
|
+
state {
|
|
84
|
+
id
|
|
85
|
+
name
|
|
86
|
+
type
|
|
87
|
+
}
|
|
88
|
+
assignee {
|
|
89
|
+
id
|
|
90
|
+
name
|
|
91
|
+
email
|
|
92
|
+
}
|
|
93
|
+
team {
|
|
94
|
+
id
|
|
95
|
+
key
|
|
96
|
+
name
|
|
97
|
+
}
|
|
98
|
+
labels {
|
|
99
|
+
nodes {
|
|
100
|
+
id
|
|
101
|
+
name
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
createdAt
|
|
105
|
+
updatedAt
|
|
106
|
+
archivedAt
|
|
107
|
+
`;
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Client Implementation
|
|
110
|
+
// ============================================================================
|
|
111
|
+
/**
|
|
112
|
+
* Fetch-based Linear GraphQL API client for issue operations.
|
|
113
|
+
*
|
|
114
|
+
* Features:
|
|
115
|
+
* - API key authentication (no Bearer prefix)
|
|
116
|
+
* - Rate limit tracking with warnings when approaching limit
|
|
117
|
+
* - Typed errors with GraphQL error details
|
|
118
|
+
* - Relay-style cursor pagination
|
|
119
|
+
* - Partial error handling (data + errors)
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const client = new LinearApiClient({ apiKey: 'lin_api_...' });
|
|
124
|
+
* const viewer = await client.getViewer();
|
|
125
|
+
* const issues = await client.listIssuesSince('ENG', '2024-01-01T00:00:00Z');
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export class LinearApiClient {
|
|
129
|
+
apiKey;
|
|
130
|
+
rateLimitWarningThreshold;
|
|
131
|
+
/** Most recently observed rate limit info (updated after each request) */
|
|
132
|
+
lastRateLimit = null;
|
|
133
|
+
constructor(options) {
|
|
134
|
+
if (!options.apiKey) {
|
|
135
|
+
throw new Error('Linear API key is required');
|
|
136
|
+
}
|
|
137
|
+
this.apiKey = options.apiKey;
|
|
138
|
+
this.rateLimitWarningThreshold =
|
|
139
|
+
options.rateLimitWarningThreshold ?? DEFAULT_RATE_LIMIT_WARNING_THRESHOLD;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Returns the most recently observed rate limit info, or null if no requests have been made.
|
|
143
|
+
*/
|
|
144
|
+
getRateLimit() {
|
|
145
|
+
return this.lastRateLimit;
|
|
146
|
+
}
|
|
147
|
+
// --------------------------------------------------------------------------
|
|
148
|
+
// Core GraphQL Method
|
|
149
|
+
// --------------------------------------------------------------------------
|
|
150
|
+
/**
|
|
151
|
+
* Performs a GraphQL request to the Linear API.
|
|
152
|
+
*
|
|
153
|
+
* Handles authentication, rate limit parsing, and error responses.
|
|
154
|
+
* For 200 responses that include both `data` and `errors`, it logs warnings
|
|
155
|
+
* for partial errors but returns the data.
|
|
156
|
+
*
|
|
157
|
+
* @throws {LinearApiError} On network errors, non-200 responses, or responses with only errors.
|
|
158
|
+
*/
|
|
159
|
+
async graphql(query, variables) {
|
|
160
|
+
let response;
|
|
161
|
+
try {
|
|
162
|
+
response = await fetch(LINEAR_API_URL, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: {
|
|
165
|
+
Authorization: this.apiKey,
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify({ query, variables }),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
throw new LinearApiError(`Network error requesting Linear API: ${err instanceof Error ? err.message : String(err)}`, 0, [], null, err instanceof Error ? err : undefined);
|
|
173
|
+
}
|
|
174
|
+
// Parse rate limit headers from every response
|
|
175
|
+
const rateLimit = parseRateLimitHeaders(response.headers);
|
|
176
|
+
if (rateLimit) {
|
|
177
|
+
this.lastRateLimit = rateLimit;
|
|
178
|
+
this.checkRateLimitWarning(rateLimit);
|
|
179
|
+
}
|
|
180
|
+
// Handle non-200 HTTP responses
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
let errorMessage;
|
|
183
|
+
let graphqlErrors = [];
|
|
184
|
+
try {
|
|
185
|
+
const body = (await response.json());
|
|
186
|
+
graphqlErrors = body.errors ?? [];
|
|
187
|
+
errorMessage =
|
|
188
|
+
graphqlErrors.length > 0
|
|
189
|
+
? graphqlErrors.map((e) => e.message).join('; ')
|
|
190
|
+
: body.message ?? `Linear API error: ${response.status} ${response.statusText}`;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
errorMessage = `Linear API error: ${response.status} ${response.statusText}`;
|
|
194
|
+
}
|
|
195
|
+
// Special messaging for rate limit exhaustion
|
|
196
|
+
if (response.status === 429 || (rateLimit && rateLimit.remaining === 0)) {
|
|
197
|
+
const resetDate = rateLimit ? new Date(rateLimit.reset * 1000).toISOString() : 'unknown';
|
|
198
|
+
errorMessage = `Linear API rate limit exhausted. Resets at ${resetDate}. ${errorMessage}`;
|
|
199
|
+
}
|
|
200
|
+
throw new LinearApiError(`Linear GraphQL request failed: ${errorMessage}`, response.status, graphqlErrors, rateLimit);
|
|
201
|
+
}
|
|
202
|
+
// Parse JSON response
|
|
203
|
+
let body;
|
|
204
|
+
try {
|
|
205
|
+
body = (await response.json());
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
throw new LinearApiError(`Failed to parse Linear API response as JSON: ${err instanceof Error ? err.message : String(err)}`, response.status, [], rateLimit, err instanceof Error ? err : undefined);
|
|
209
|
+
}
|
|
210
|
+
// Handle GraphQL errors
|
|
211
|
+
if (body.errors && body.errors.length > 0) {
|
|
212
|
+
if (!body.data) {
|
|
213
|
+
// Complete failure — no data returned
|
|
214
|
+
throw new LinearApiError(`Linear GraphQL errors: ${body.errors.map((e) => e.message).join('; ')}`, response.status, body.errors, rateLimit);
|
|
215
|
+
}
|
|
216
|
+
// Partial errors — data exists alongside errors. Log warning but return data.
|
|
217
|
+
console.warn(`[LinearApiClient] Partial GraphQL errors: ${body.errors.map((e) => e.message).join('; ')}`);
|
|
218
|
+
}
|
|
219
|
+
if (!body.data) {
|
|
220
|
+
throw new LinearApiError('Linear API returned a response with no data', response.status, body.errors ?? [], rateLimit);
|
|
221
|
+
}
|
|
222
|
+
return body.data;
|
|
223
|
+
}
|
|
224
|
+
// --------------------------------------------------------------------------
|
|
225
|
+
// Public API Methods
|
|
226
|
+
// --------------------------------------------------------------------------
|
|
227
|
+
/**
|
|
228
|
+
* Fetch the authenticated user's identity.
|
|
229
|
+
* Useful for testing the API connection.
|
|
230
|
+
*
|
|
231
|
+
* @returns Viewer info with id, name, and email.
|
|
232
|
+
*/
|
|
233
|
+
async getViewer() {
|
|
234
|
+
const query = `
|
|
235
|
+
query Viewer {
|
|
236
|
+
viewer {
|
|
237
|
+
id
|
|
238
|
+
name
|
|
239
|
+
email
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
const data = await this.graphql(query);
|
|
244
|
+
return data.viewer;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* List all teams accessible to the authenticated user.
|
|
248
|
+
*
|
|
249
|
+
* @returns Array of teams with id, key, and name.
|
|
250
|
+
*/
|
|
251
|
+
async getTeams() {
|
|
252
|
+
const query = `
|
|
253
|
+
query Teams {
|
|
254
|
+
teams {
|
|
255
|
+
nodes {
|
|
256
|
+
id
|
|
257
|
+
key
|
|
258
|
+
name
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
`;
|
|
263
|
+
const data = await this.graphql(query);
|
|
264
|
+
return data.teams.nodes;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Fetch all workflow states for a specific team.
|
|
268
|
+
* Needed for mapping between Linear state types and Stoneforge statuses.
|
|
269
|
+
*
|
|
270
|
+
* @param teamId - UUID of the team.
|
|
271
|
+
* @returns Array of workflow states.
|
|
272
|
+
*/
|
|
273
|
+
async getTeamWorkflowStates(teamId) {
|
|
274
|
+
const query = `
|
|
275
|
+
query TeamWorkflowStates($teamId: String!) {
|
|
276
|
+
team(id: $teamId) {
|
|
277
|
+
states {
|
|
278
|
+
nodes {
|
|
279
|
+
id
|
|
280
|
+
name
|
|
281
|
+
type
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
const data = await this.graphql(query, { teamId });
|
|
288
|
+
return data.team.states.nodes;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Fetch all labels (issue labels) in the workspace.
|
|
292
|
+
* Labels in Linear are workspace-scoped and can be filtered by team.
|
|
293
|
+
*
|
|
294
|
+
* @returns Array of all labels with id and name.
|
|
295
|
+
*/
|
|
296
|
+
async getLabels() {
|
|
297
|
+
const query = `
|
|
298
|
+
query IssueLabels {
|
|
299
|
+
issueLabels {
|
|
300
|
+
nodes {
|
|
301
|
+
id
|
|
302
|
+
name
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
const data = await this.graphql(query);
|
|
308
|
+
return data.issueLabels.nodes;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Create a new label in the workspace, optionally associated with a team.
|
|
312
|
+
*
|
|
313
|
+
* @param name - Label name (e.g., "blocked")
|
|
314
|
+
* @param teamId - Optional team ID to associate the label with
|
|
315
|
+
* @returns The created label with id and name.
|
|
316
|
+
*/
|
|
317
|
+
async createLabel(name, teamId) {
|
|
318
|
+
const query = `
|
|
319
|
+
mutation CreateLabel($input: IssueLabelCreateInput!) {
|
|
320
|
+
issueLabelCreate(input: $input) {
|
|
321
|
+
success
|
|
322
|
+
issueLabel {
|
|
323
|
+
id
|
|
324
|
+
name
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
`;
|
|
329
|
+
const input = { name };
|
|
330
|
+
if (teamId) {
|
|
331
|
+
input.teamId = teamId;
|
|
332
|
+
}
|
|
333
|
+
const data = await this.graphql(query, { input });
|
|
334
|
+
if (!data.issueLabelCreate.success) {
|
|
335
|
+
throw new LinearApiError(`Linear issueLabelCreate mutation returned success: false for label "${name}"`, 200, [], this.lastRateLimit);
|
|
336
|
+
}
|
|
337
|
+
return data.issueLabelCreate.issueLabel;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Fetch a single issue by UUID or identifier (e.g., "ENG-123").
|
|
341
|
+
*
|
|
342
|
+
* @param issueId - UUID or human-readable identifier.
|
|
343
|
+
* @returns The issue, or null if not found.
|
|
344
|
+
*/
|
|
345
|
+
async getIssue(issueId) {
|
|
346
|
+
const query = `
|
|
347
|
+
query Issue($issueId: String!) {
|
|
348
|
+
issue(id: $issueId) {
|
|
349
|
+
${ISSUE_FIELDS}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
`;
|
|
353
|
+
try {
|
|
354
|
+
const data = await this.graphql(query, { issueId });
|
|
355
|
+
return data.issue;
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
// Linear returns errors for not-found issues rather than null
|
|
359
|
+
if (err instanceof LinearApiError && err.graphqlErrors.length > 0) {
|
|
360
|
+
const notFound = err.graphqlErrors.some((e) => e.message.toLowerCase().includes('not found') ||
|
|
361
|
+
e.extensions?.code === 'RESOURCE_NOT_FOUND');
|
|
362
|
+
if (notFound) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* List issues for a team updated since a given timestamp.
|
|
371
|
+
* Automatically paginates through all results using cursor pagination.
|
|
372
|
+
*
|
|
373
|
+
* @param teamKey - Team key (e.g., "ENG").
|
|
374
|
+
* @param since - ISO 8601 timestamp. Only issues updated at or after this time are returned.
|
|
375
|
+
* @returns Array of all matching issues.
|
|
376
|
+
*/
|
|
377
|
+
async listIssuesSince(teamKey, since) {
|
|
378
|
+
const query = `
|
|
379
|
+
query ListIssues($teamKey: String!, $since: DateTimeOrDuration!, $after: String) {
|
|
380
|
+
issues(
|
|
381
|
+
filter: {
|
|
382
|
+
team: { key: { eq: $teamKey } }
|
|
383
|
+
updatedAt: { gte: $since }
|
|
384
|
+
}
|
|
385
|
+
first: 50
|
|
386
|
+
after: $after
|
|
387
|
+
) {
|
|
388
|
+
nodes {
|
|
389
|
+
${ISSUE_FIELDS}
|
|
390
|
+
}
|
|
391
|
+
pageInfo {
|
|
392
|
+
hasNextPage
|
|
393
|
+
endCursor
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
`;
|
|
398
|
+
const allIssues = [];
|
|
399
|
+
let after = null;
|
|
400
|
+
// eslint-disable-next-line no-constant-condition
|
|
401
|
+
while (true) {
|
|
402
|
+
const variables = { teamKey, since };
|
|
403
|
+
if (after) {
|
|
404
|
+
variables.after = after;
|
|
405
|
+
}
|
|
406
|
+
const data = await this.graphql(query, variables);
|
|
407
|
+
allIssues.push(...data.issues.nodes);
|
|
408
|
+
if (!data.issues.pageInfo.hasNextPage) {
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
after = data.issues.pageInfo.endCursor;
|
|
412
|
+
}
|
|
413
|
+
return allIssues;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Create a new issue in Linear.
|
|
417
|
+
*
|
|
418
|
+
* @param input - Issue creation input (teamId and title required).
|
|
419
|
+
* @returns The created issue.
|
|
420
|
+
*/
|
|
421
|
+
async createIssue(input) {
|
|
422
|
+
const query = `
|
|
423
|
+
mutation CreateIssue($input: IssueCreateInput!) {
|
|
424
|
+
issueCreate(input: $input) {
|
|
425
|
+
success
|
|
426
|
+
issue {
|
|
427
|
+
${ISSUE_FIELDS}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
432
|
+
const data = await this.graphql(query, { input });
|
|
433
|
+
if (!data.issueCreate.success) {
|
|
434
|
+
throw new LinearApiError('Linear issueCreate mutation returned success: false', 200, [], this.lastRateLimit);
|
|
435
|
+
}
|
|
436
|
+
return data.issueCreate.issue;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Update an existing issue in Linear.
|
|
440
|
+
*
|
|
441
|
+
* @param issueId - UUID of the issue to update.
|
|
442
|
+
* @param input - Fields to update.
|
|
443
|
+
* @returns The updated issue.
|
|
444
|
+
*/
|
|
445
|
+
async updateIssue(issueId, input) {
|
|
446
|
+
const query = `
|
|
447
|
+
mutation UpdateIssue($issueId: String!, $input: IssueUpdateInput!) {
|
|
448
|
+
issueUpdate(id: $issueId, input: $input) {
|
|
449
|
+
success
|
|
450
|
+
issue {
|
|
451
|
+
${ISSUE_FIELDS}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
`;
|
|
456
|
+
const data = await this.graphql(query, { issueId, input });
|
|
457
|
+
if (!data.issueUpdate.success) {
|
|
458
|
+
throw new LinearApiError('Linear issueUpdate mutation returned success: false', 200, [], this.lastRateLimit);
|
|
459
|
+
}
|
|
460
|
+
return data.issueUpdate.issue;
|
|
461
|
+
}
|
|
462
|
+
// --------------------------------------------------------------------------
|
|
463
|
+
// Internal: Rate Limit Handling
|
|
464
|
+
// --------------------------------------------------------------------------
|
|
465
|
+
/**
|
|
466
|
+
* Logs a warning when rate limit is approaching exhaustion.
|
|
467
|
+
*/
|
|
468
|
+
checkRateLimitWarning(rateLimit) {
|
|
469
|
+
if (rateLimit.remaining <= this.rateLimitWarningThreshold && rateLimit.remaining > 0) {
|
|
470
|
+
const resetDate = new Date(rateLimit.reset * 1000);
|
|
471
|
+
console.warn(`[LinearApiClient] Rate limit warning: ${rateLimit.remaining}/${rateLimit.limit} requests remaining. ` +
|
|
472
|
+
`Resets at ${resetDate.toISOString()}.`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// ============================================================================
|
|
477
|
+
// Utility Functions
|
|
478
|
+
// ============================================================================
|
|
479
|
+
/**
|
|
480
|
+
* Parses rate limit information from Linear response headers.
|
|
481
|
+
*
|
|
482
|
+
* Linear uses these headers:
|
|
483
|
+
* - X-RateLimit-Requests-Limit
|
|
484
|
+
* - X-RateLimit-Requests-Remaining
|
|
485
|
+
* - X-RateLimit-Requests-Reset (epoch seconds)
|
|
486
|
+
*
|
|
487
|
+
* Also checks standard headers as fallback:
|
|
488
|
+
* - X-RateLimit-Limit
|
|
489
|
+
* - X-RateLimit-Remaining
|
|
490
|
+
* - X-RateLimit-Reset
|
|
491
|
+
*
|
|
492
|
+
* @returns Parsed rate limit info, or null if headers are not present.
|
|
493
|
+
*/
|
|
494
|
+
function parseRateLimitHeaders(headers) {
|
|
495
|
+
// Try Linear-specific headers first
|
|
496
|
+
let limit = headers.get('X-RateLimit-Requests-Limit');
|
|
497
|
+
let remaining = headers.get('X-RateLimit-Requests-Remaining');
|
|
498
|
+
let reset = headers.get('X-RateLimit-Requests-Reset');
|
|
499
|
+
// Fall back to standard headers
|
|
500
|
+
if (limit === null || remaining === null || reset === null) {
|
|
501
|
+
limit = headers.get('X-RateLimit-Limit');
|
|
502
|
+
remaining = headers.get('X-RateLimit-Remaining');
|
|
503
|
+
reset = headers.get('X-RateLimit-Reset');
|
|
504
|
+
}
|
|
505
|
+
if (limit === null || remaining === null || reset === null) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
const parsedLimit = parseInt(limit, 10);
|
|
509
|
+
const parsedRemaining = parseInt(remaining, 10);
|
|
510
|
+
const parsedReset = parseInt(reset, 10);
|
|
511
|
+
if (isNaN(parsedLimit) || isNaN(parsedRemaining) || isNaN(parsedReset)) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
limit: parsedLimit,
|
|
516
|
+
remaining: parsedRemaining,
|
|
517
|
+
reset: parsedReset,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
// Export utility function for testing
|
|
521
|
+
export { parseRateLimitHeaders };
|
|
522
|
+
//# sourceMappingURL=linear-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-api.js","sourceRoot":"","sources":["../../../../src/external-sync/providers/linear/linear-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Field Mapping Configuration
|
|
3
|
+
*
|
|
4
|
+
* Defines how Stoneforge task fields map to Linear issue fields.
|
|
5
|
+
*
|
|
6
|
+
* Key differences from GitHub:
|
|
7
|
+
* - Linear has native priority (0-4) — no label-based priority convention needed
|
|
8
|
+
* - Linear has workflow state types — status mapping uses state types, not labels
|
|
9
|
+
* - Task types use the same label convention as GitHub (type:bug, etc.)
|
|
10
|
+
*
|
|
11
|
+
* Priority mapping (Linear 0-4 ↔ Stoneforge 1-5):
|
|
12
|
+
* Linear 1 (Urgent) ↔ Stoneforge 1 (critical)
|
|
13
|
+
* Linear 2 (High) ↔ Stoneforge 2 (high)
|
|
14
|
+
* Linear 3 (Medium) ↔ Stoneforge 3 (medium)
|
|
15
|
+
* Linear 4 (Low) ↔ Stoneforge 4 (low)
|
|
16
|
+
* Linear 0 (No priority) ↔ Stoneforge 5 (minimal)
|
|
17
|
+
*
|
|
18
|
+
* Status mapping via workflow state TYPE (not name):
|
|
19
|
+
* Pull: triage/backlog → backlog, unstarted → open, started → in_progress,
|
|
20
|
+
* completed → closed, canceled → closed (closeReason: "canceled")
|
|
21
|
+
* Push: open → unstarted, in_progress/review → started,
|
|
22
|
+
* blocked → started (add "blocked" label), deferred/backlog → backlog,
|
|
23
|
+
* closed → completed
|
|
24
|
+
*/
|
|
25
|
+
import type { TaskStatus, Priority } from '@stoneforge/core';
|
|
26
|
+
import type { TaskFieldMapConfig } from '@stoneforge/core';
|
|
27
|
+
import type { TaskSyncFieldMapConfig } from '../../adapters/task-sync-adapter.js';
|
|
28
|
+
import type { LinearWorkflowState } from './linear-types.js';
|
|
29
|
+
/**
|
|
30
|
+
* Maps Stoneforge TaskStatus values to label strings.
|
|
31
|
+
* Linear doesn't natively use label-based status, but the adapter injects
|
|
32
|
+
* sf:status:* labels into ExternalTask.labels to communicate granular workflow
|
|
33
|
+
* state types through the generic field mapping system.
|
|
34
|
+
*
|
|
35
|
+
* This keeps the sync engine provider-agnostic — it reads status from labels
|
|
36
|
+
* the same way for both GitHub (user-managed labels) and Linear (adapter-injected).
|
|
37
|
+
*/
|
|
38
|
+
export declare const LINEAR_STATUS_LABELS: Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* Maps Stoneforge priority (1-5) to Linear priority (0-4).
|
|
41
|
+
*
|
|
42
|
+
* Stoneforge 1 (critical) → Linear 1 (Urgent)
|
|
43
|
+
* Stoneforge 2 (high) → Linear 2 (High)
|
|
44
|
+
* Stoneforge 3 (medium) → Linear 3 (Medium)
|
|
45
|
+
* Stoneforge 4 (low) → Linear 4 (Low)
|
|
46
|
+
* Stoneforge 5 (minimal) → Linear 0 (No priority)
|
|
47
|
+
*/
|
|
48
|
+
export declare function stoneforgePriorityToLinear(priority: Priority): number;
|
|
49
|
+
/**
|
|
50
|
+
* Maps Linear priority (0-4) to Stoneforge priority (1-5).
|
|
51
|
+
*
|
|
52
|
+
* Linear 1 (Urgent) → Stoneforge 1 (critical)
|
|
53
|
+
* Linear 2 (High) → Stoneforge 2 (high)
|
|
54
|
+
* Linear 3 (Medium) → Stoneforge 3 (medium)
|
|
55
|
+
* Linear 4 (Low) → Stoneforge 4 (low)
|
|
56
|
+
* Linear 0 (No priority) → Stoneforge 5 (minimal)
|
|
57
|
+
*/
|
|
58
|
+
export declare function linearPriorityToStoneforge(priority: number): Priority;
|
|
59
|
+
/**
|
|
60
|
+
* Linear workflow state type as used in the API.
|
|
61
|
+
*/
|
|
62
|
+
export type LinearStateType = LinearWorkflowState['type'];
|
|
63
|
+
/**
|
|
64
|
+
* Maps a Linear workflow state type to a Stoneforge TaskStatus.
|
|
65
|
+
*
|
|
66
|
+
* Pull direction:
|
|
67
|
+
* triage → backlog
|
|
68
|
+
* backlog → backlog
|
|
69
|
+
* unstarted → open
|
|
70
|
+
* started → in_progress
|
|
71
|
+
* completed → closed
|
|
72
|
+
* canceled → closed
|
|
73
|
+
*/
|
|
74
|
+
export declare function linearStateTypeToStatus(stateType: LinearStateType): {
|
|
75
|
+
status: TaskStatus;
|
|
76
|
+
closeReason?: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Maps a Stoneforge TaskStatus to a Linear workflow state type.
|
|
80
|
+
*
|
|
81
|
+
* Push direction:
|
|
82
|
+
* open → unstarted
|
|
83
|
+
* in_progress → started
|
|
84
|
+
* review → started
|
|
85
|
+
* blocked → started (caller should add "blocked" label)
|
|
86
|
+
* deferred → backlog
|
|
87
|
+
* backlog → backlog
|
|
88
|
+
* closed → completed
|
|
89
|
+
* tombstone → completed (treat as done)
|
|
90
|
+
*/
|
|
91
|
+
export declare function statusToLinearStateType(status: TaskStatus): LinearStateType;
|
|
92
|
+
/**
|
|
93
|
+
* Returns true if the given Stoneforge status should add a "blocked" label
|
|
94
|
+
* in Linear (since Linear has no native blocked state).
|
|
95
|
+
*/
|
|
96
|
+
export declare function shouldAddBlockedLabel(status: TaskStatus): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Maps a Linear workflow state type to a sync label string (sf:status:*).
|
|
99
|
+
*
|
|
100
|
+
* The Linear adapter injects this label into ExternalTask.labels so the
|
|
101
|
+
* generic field mapping system (stateToStatus) can extract granular statuses
|
|
102
|
+
* without the sync engine needing to know about Linear-specific state types.
|
|
103
|
+
*
|
|
104
|
+
* Mapping:
|
|
105
|
+
* triage → sf:status:backlog
|
|
106
|
+
* backlog → sf:status:backlog
|
|
107
|
+
* unstarted → sf:status:open
|
|
108
|
+
* started → sf:status:in-progress
|
|
109
|
+
* completed → sf:status:closed
|
|
110
|
+
* canceled → sf:status:closed
|
|
111
|
+
*/
|
|
112
|
+
export declare function linearStateTypeToStatusLabel(stateType: LinearStateType): string;
|
|
113
|
+
/**
|
|
114
|
+
* Creates the Linear-specific TaskFieldMapConfig.
|
|
115
|
+
*
|
|
116
|
+
* This config describes the field mapping at the type level. The actual
|
|
117
|
+
* runtime mapping logic lives in the adapter and the priority/status
|
|
118
|
+
* functions above.
|
|
119
|
+
*/
|
|
120
|
+
export declare function createLinearFieldMapConfig(): TaskFieldMapConfig;
|
|
121
|
+
/**
|
|
122
|
+
* Creates the Linear-specific TaskSyncFieldMapConfig used by the shared
|
|
123
|
+
* task-sync-adapter utilities (taskToExternalTask, externalTaskToTaskUpdates).
|
|
124
|
+
*
|
|
125
|
+
* Priority labels are emitted alongside Linear's native priority field for
|
|
126
|
+
* lossless round-tripping. This ensures the exact Stoneforge priority value
|
|
127
|
+
* is preserved (e.g., P5 "minimal" is distinct from Linear's "No priority").
|
|
128
|
+
*
|
|
129
|
+
* Status mapping uses adapter-injected sf:status:* labels. The Linear adapter
|
|
130
|
+
* injects these labels based on the workflow state type (e.g., started →
|
|
131
|
+
* sf:status:in-progress), allowing the generic stateToStatus function to
|
|
132
|
+
* extract granular statuses without coupling the sync engine to Linear.
|
|
133
|
+
*/
|
|
134
|
+
export declare function createLinearSyncFieldMapConfig(): TaskSyncFieldMapConfig;
|
|
135
|
+
//# sourceMappingURL=linear-field-map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-field-map.d.ts","sourceRoot":"","sources":["../../../../src/external-sync/providers/linear/linear-field-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAM7D;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CASvD,CAAC;AAMF;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAerE;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAerE;AAMD;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,eAAe,GAAG;IACnE,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAiBA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,UAAU,GAAG,eAAe,CAqB3E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAEjE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,eAAe,GAAG,MAAM,CAiB/E;AAMD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,IAAI,kBAAkB,CAiD/D;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,8BAA8B,IAAI,sBAAsB,CAgEvE"}
|