@shakenbake/linear 0.0.1
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/dist/__tests__/adapter.test.d.ts +2 -0
- package/dist/__tests__/adapter.test.d.ts.map +1 -0
- package/dist/__tests__/adapter.test.js +441 -0
- package/dist/__tests__/adapter.test.js.map +1 -0
- package/dist/__tests__/graphql.test.d.ts +2 -0
- package/dist/__tests__/graphql.test.d.ts.map +1 -0
- package/dist/__tests__/graphql.test.js +253 -0
- package/dist/__tests__/graphql.test.js.map +1 -0
- package/dist/__tests__/markdown.test.d.ts +2 -0
- package/dist/__tests__/markdown.test.d.ts.map +1 -0
- package/dist/__tests__/markdown.test.js +160 -0
- package/dist/__tests__/markdown.test.js.map +1 -0
- package/dist/__tests__/upload.test.d.ts +2 -0
- package/dist/__tests__/upload.test.d.ts.map +1 -0
- package/dist/__tests__/upload.test.js +355 -0
- package/dist/__tests__/upload.test.js.map +1 -0
- package/dist/adapter.d.ts +53 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +237 -0
- package/dist/adapter.js.map +1 -0
- package/dist/graphql.d.ts +64 -0
- package/dist/graphql.d.ts.map +1 -0
- package/dist/graphql.js +120 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +165 -0
- package/dist/markdown.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +30 -0
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// @shakenbake/linear — LinearAdapter implementing DestinationAdapter
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LinearAdapter = void 0;
|
|
7
|
+
const core_1 = require("@shakenbake/core");
|
|
8
|
+
const types_js_1 = require("./types.js");
|
|
9
|
+
const graphql_js_1 = require("./graphql.js");
|
|
10
|
+
const markdown_js_1 = require("./markdown.js");
|
|
11
|
+
/**
|
|
12
|
+
* Linear destination adapter.
|
|
13
|
+
*
|
|
14
|
+
* Creates issues in Linear via the GraphQL API with markdown descriptions,
|
|
15
|
+
* embedded screenshots, and full device context.
|
|
16
|
+
*/
|
|
17
|
+
class LinearAdapter {
|
|
18
|
+
name = 'linear';
|
|
19
|
+
config;
|
|
20
|
+
apiUrl;
|
|
21
|
+
severityMapping;
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.apiUrl = config.apiUrl ?? types_js_1.DEFAULT_API_URL;
|
|
25
|
+
this.severityMapping = config.severityMapping ?? types_js_1.DEFAULT_SEVERITY_MAPPING;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Upload an image to Linear via the two-step file upload flow:
|
|
29
|
+
* 1. Call fileUpload mutation to get a signed upload URL and asset URL
|
|
30
|
+
* 2. PUT the image data to the signed URL with headers from response
|
|
31
|
+
* 3. Return the asset URL for embedding in the issue
|
|
32
|
+
*
|
|
33
|
+
* @throws {ShakeNbakeError} with code UPLOAD_FAILED on failure
|
|
34
|
+
*/
|
|
35
|
+
async uploadImage(imageData, filename) {
|
|
36
|
+
const size = imageData instanceof Blob ? imageData.size : imageData.byteLength;
|
|
37
|
+
const contentType = LinearAdapter.detectContentType(filename);
|
|
38
|
+
// Step 1: Request a signed upload URL from Linear
|
|
39
|
+
let uploadUrl;
|
|
40
|
+
let assetUrl;
|
|
41
|
+
let uploadHeaders;
|
|
42
|
+
try {
|
|
43
|
+
const result = await (0, graphql_js_1.requestUploadUrl)(this.config.apiKey, this.apiUrl, filename, contentType, size);
|
|
44
|
+
uploadUrl = result.uploadUrl;
|
|
45
|
+
assetUrl = result.assetUrl;
|
|
46
|
+
uploadHeaders = result.headers;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error instanceof core_1.ShakeNbakeError) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
throw new core_1.ShakeNbakeError('Failed to initiate file upload to Linear', 'UPLOAD_FAILED', { originalError: error });
|
|
53
|
+
}
|
|
54
|
+
// Step 2: PUT the image data to the signed URL
|
|
55
|
+
try {
|
|
56
|
+
// Convert Buffer to Blob for fetch body compatibility across environments
|
|
57
|
+
const body = imageData instanceof Blob
|
|
58
|
+
? imageData
|
|
59
|
+
: new Blob([new Uint8Array(imageData)], { type: contentType });
|
|
60
|
+
// Build headers: Content-Type + Cache-Control + any headers from Linear
|
|
61
|
+
const putHeaders = {
|
|
62
|
+
'Content-Type': contentType,
|
|
63
|
+
'Cache-Control': 'public, max-age=31536000',
|
|
64
|
+
};
|
|
65
|
+
// Apply additional headers returned by the fileUpload mutation
|
|
66
|
+
for (const header of uploadHeaders) {
|
|
67
|
+
putHeaders[header.key] = header.value;
|
|
68
|
+
}
|
|
69
|
+
const putResponse = await fetch(uploadUrl, {
|
|
70
|
+
method: 'PUT',
|
|
71
|
+
headers: putHeaders,
|
|
72
|
+
body,
|
|
73
|
+
});
|
|
74
|
+
if (!putResponse.ok) {
|
|
75
|
+
throw new core_1.ShakeNbakeError(`File upload to Linear storage failed (HTTP ${String(putResponse.status)})`, 'UPLOAD_FAILED', { retryable: false });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof core_1.ShakeNbakeError) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
throw new core_1.ShakeNbakeError('Network error during file upload to Linear storage', 'UPLOAD_FAILED', { originalError: error });
|
|
83
|
+
}
|
|
84
|
+
return assetUrl;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Detect the content type from a filename extension.
|
|
88
|
+
*/
|
|
89
|
+
static detectContentType(filename) {
|
|
90
|
+
const lower = filename.toLowerCase();
|
|
91
|
+
if (lower.endsWith('.png'))
|
|
92
|
+
return 'image/png';
|
|
93
|
+
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg'))
|
|
94
|
+
return 'image/jpeg';
|
|
95
|
+
if (lower.endsWith('.gif'))
|
|
96
|
+
return 'image/gif';
|
|
97
|
+
if (lower.endsWith('.webp'))
|
|
98
|
+
return 'image/webp';
|
|
99
|
+
if (lower.endsWith('.webm'))
|
|
100
|
+
return 'audio/webm';
|
|
101
|
+
if (lower.endsWith('.m4a'))
|
|
102
|
+
return 'audio/m4a';
|
|
103
|
+
if (lower.endsWith('.svg'))
|
|
104
|
+
return 'image/svg+xml';
|
|
105
|
+
return 'application/octet-stream';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Create a Linear issue from a BugReport.
|
|
109
|
+
*
|
|
110
|
+
* Uploads screenshots, builds a markdown description, and creates the issue
|
|
111
|
+
* via the issueCreate GraphQL mutation.
|
|
112
|
+
*
|
|
113
|
+
* @throws {ShakeNbakeError} with appropriate code on failure
|
|
114
|
+
*/
|
|
115
|
+
async createIssue(report) {
|
|
116
|
+
// Upload screenshots
|
|
117
|
+
let annotatedUrl;
|
|
118
|
+
let originalUrl;
|
|
119
|
+
let audioUrl;
|
|
120
|
+
try {
|
|
121
|
+
annotatedUrl = await this.uploadImage(base64ToBuffer(report.screenshot.annotated), `screenshot-annotated-${report.id}.png`);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Fall back to embedding base64 inline if upload fails
|
|
125
|
+
annotatedUrl = undefined;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
originalUrl = await this.uploadImage(base64ToBuffer(report.screenshot.original), `screenshot-original-${report.id}.png`);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
originalUrl = undefined;
|
|
132
|
+
}
|
|
133
|
+
// Upload audio if present
|
|
134
|
+
if (report.audio?.data) {
|
|
135
|
+
try {
|
|
136
|
+
const ext = report.audio.mimeType.includes('webm') ? 'webm' : 'm4a';
|
|
137
|
+
audioUrl = await this.uploadImage(base64ToBuffer(report.audio.data), `audio-${report.id}.${ext}`);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
audioUrl = undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Build markdown description
|
|
144
|
+
const description = (0, markdown_js_1.buildIssueDescription)(report, annotatedUrl, originalUrl, audioUrl);
|
|
145
|
+
// Determine priority
|
|
146
|
+
const priority = this.resolvePriority(report.severity);
|
|
147
|
+
// Build label IDs
|
|
148
|
+
const labelIds = this.resolveLabelIds(report.category);
|
|
149
|
+
// Build input
|
|
150
|
+
const input = {
|
|
151
|
+
title: report.title,
|
|
152
|
+
description,
|
|
153
|
+
teamId: this.config.teamId,
|
|
154
|
+
};
|
|
155
|
+
if (this.config.projectId) {
|
|
156
|
+
input['projectId'] = this.config.projectId;
|
|
157
|
+
}
|
|
158
|
+
if (labelIds.length > 0) {
|
|
159
|
+
input['labelIds'] = labelIds;
|
|
160
|
+
}
|
|
161
|
+
if (this.config.defaultAssigneeId) {
|
|
162
|
+
input['assigneeId'] = this.config.defaultAssigneeId;
|
|
163
|
+
}
|
|
164
|
+
if (priority !== undefined) {
|
|
165
|
+
input['priority'] = priority;
|
|
166
|
+
}
|
|
167
|
+
// Create issue
|
|
168
|
+
const data = await (0, graphql_js_1.linearFetch)(this.config.apiKey, this.apiUrl, graphql_js_1.ISSUE_CREATE_MUTATION, { input });
|
|
169
|
+
if (!data.issueCreate.success) {
|
|
170
|
+
throw new core_1.ShakeNbakeError('Linear issue creation returned success=false', 'UNKNOWN', { retryable: false });
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
url: data.issueCreate.issue.url,
|
|
174
|
+
id: data.issueCreate.issue.id,
|
|
175
|
+
success: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Test the connection to Linear by querying the authenticated user.
|
|
180
|
+
*
|
|
181
|
+
* @returns true if credentials are valid, false if auth fails
|
|
182
|
+
* @throws {ShakeNbakeError} with NETWORK_ERROR for connectivity issues
|
|
183
|
+
*/
|
|
184
|
+
async testConnection() {
|
|
185
|
+
try {
|
|
186
|
+
const data = await (0, graphql_js_1.linearFetch)(this.config.apiKey, this.apiUrl, graphql_js_1.VIEWER_QUERY);
|
|
187
|
+
return Boolean(data.viewer?.id);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (error instanceof core_1.ShakeNbakeError) {
|
|
191
|
+
if (error.code === 'AUTH_FAILED') {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
throw new core_1.ShakeNbakeError('Failed to test Linear connection', 'NETWORK_ERROR', { originalError: error });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Resolve the Linear priority from a severity string.
|
|
201
|
+
*/
|
|
202
|
+
resolvePriority(severity) {
|
|
203
|
+
const mapped = this.severityMapping[severity];
|
|
204
|
+
if (mapped !== undefined) {
|
|
205
|
+
return mapped;
|
|
206
|
+
}
|
|
207
|
+
return this.config.defaultPriority;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve label IDs from default labels + category-based labels.
|
|
211
|
+
*/
|
|
212
|
+
resolveLabelIds(category) {
|
|
213
|
+
const ids = [];
|
|
214
|
+
if (this.config.defaultLabelIds) {
|
|
215
|
+
ids.push(...this.config.defaultLabelIds);
|
|
216
|
+
}
|
|
217
|
+
if (this.config.categoryLabels) {
|
|
218
|
+
const categoryLabel = this.config.categoryLabels[category];
|
|
219
|
+
if (categoryLabel) {
|
|
220
|
+
ids.push(categoryLabel);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return ids;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
exports.LinearAdapter = LinearAdapter;
|
|
227
|
+
/**
|
|
228
|
+
* Convert a base64-encoded string to a Buffer.
|
|
229
|
+
* Strips the data URI prefix if present.
|
|
230
|
+
*/
|
|
231
|
+
function base64ToBuffer(base64) {
|
|
232
|
+
// Remove data URI prefix if present (e.g., "data:image/png;base64,")
|
|
233
|
+
const commaIndex = base64.indexOf(',');
|
|
234
|
+
const raw = commaIndex >= 0 ? base64.substring(commaIndex + 1) : base64;
|
|
235
|
+
return Buffer.from(raw, 'base64');
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export declare const VIEWER_QUERY = "\n query Viewer {\n viewer {\n id\n }\n }\n";
|
|
2
|
+
export declare const ISSUE_CREATE_MUTATION = "\n mutation IssueCreate($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n id\n identifier\n title\n url\n }\n }\n }\n";
|
|
3
|
+
export declare const FILE_UPLOAD_MUTATION = "\n mutation FileUpload($size: Int!, $contentType: String!, $filename: String!) {\n fileUpload(size: $size, contentType: $contentType, filename: $filename) {\n success\n uploadFile {\n uploadUrl\n assetUrl\n headers {\n key\n value\n }\n }\n }\n }\n";
|
|
4
|
+
export interface GraphQLResponse<T = unknown> {
|
|
5
|
+
data?: T;
|
|
6
|
+
errors?: Array<{
|
|
7
|
+
message: string;
|
|
8
|
+
extensions?: Record<string, unknown>;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export interface ViewerData {
|
|
12
|
+
viewer: {
|
|
13
|
+
id: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface IssueCreateData {
|
|
17
|
+
issueCreate: {
|
|
18
|
+
success: boolean;
|
|
19
|
+
issue: {
|
|
20
|
+
id: string;
|
|
21
|
+
identifier: string;
|
|
22
|
+
title: string;
|
|
23
|
+
url: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface FileUploadHeader {
|
|
28
|
+
key: string;
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
export interface FileUploadData {
|
|
32
|
+
fileUpload: {
|
|
33
|
+
success: boolean;
|
|
34
|
+
uploadFile: {
|
|
35
|
+
uploadUrl: string;
|
|
36
|
+
assetUrl: string;
|
|
37
|
+
headers: FileUploadHeader[];
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Result of requesting an upload URL from Linear.
|
|
43
|
+
*/
|
|
44
|
+
export interface UploadUrlResult {
|
|
45
|
+
uploadUrl: string;
|
|
46
|
+
assetUrl: string;
|
|
47
|
+
headers: FileUploadHeader[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Request a signed upload URL from Linear's fileUpload mutation.
|
|
51
|
+
*
|
|
52
|
+
* @throws {ShakeNbakeError} on failure
|
|
53
|
+
*/
|
|
54
|
+
export declare function requestUploadUrl(apiKey: string, apiUrl: string, filename: string, contentType: string, size: number): Promise<UploadUrlResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Execute a GraphQL request against the Linear API.
|
|
57
|
+
*
|
|
58
|
+
* Authorization header uses the API key directly (no "Bearer" prefix),
|
|
59
|
+
* matching Linear's expected format.
|
|
60
|
+
*
|
|
61
|
+
* @throws {ShakeNbakeError} with appropriate code on failure
|
|
62
|
+
*/
|
|
63
|
+
export declare function linearFetch<T = unknown>(apiKey: string, apiUrl: string, query: string, variables?: Record<string, unknown>): Promise<T>;
|
|
64
|
+
//# sourceMappingURL=graphql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../src/graphql.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,YAAY,6DAMxB,CAAC;AAEF,eAAO,MAAM,qBAAqB,6MAYjC,CAAC;AAEF,eAAO,MAAM,oBAAoB,kUAchC,CAAC;AAIF,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;CAC3E;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE;QACX,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,EAAE;YACL,EAAE,EAAE,MAAM,CAAC;YACX,UAAU,EAAE,MAAM,CAAC;YACnB,KAAK,EAAE,MAAM,CAAC;YACd,GAAG,EAAE,MAAM,CAAC;SACb,CAAC;KACH,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE;QACV,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,EAAE;YACV,SAAS,EAAE,MAAM,CAAC;YAClB,QAAQ,EAAE,MAAM,CAAC;YACjB,OAAO,EAAE,gBAAgB,EAAE,CAAC;SAC7B,CAAC;KACH,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,CAAC,CAkB1B;AAID;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,CAAC,GAAG,OAAO,EAC3C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAgGZ"}
|
package/dist/graphql.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// @shakenbake/linear — GraphQL queries, mutations, and fetch wrapper
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.FILE_UPLOAD_MUTATION = exports.ISSUE_CREATE_MUTATION = exports.VIEWER_QUERY = void 0;
|
|
7
|
+
exports.requestUploadUrl = requestUploadUrl;
|
|
8
|
+
exports.linearFetch = linearFetch;
|
|
9
|
+
const core_1 = require("@shakenbake/core");
|
|
10
|
+
// ---- GraphQL Queries & Mutations ----
|
|
11
|
+
exports.VIEWER_QUERY = `
|
|
12
|
+
query Viewer {
|
|
13
|
+
viewer {
|
|
14
|
+
id
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
exports.ISSUE_CREATE_MUTATION = `
|
|
19
|
+
mutation IssueCreate($input: IssueCreateInput!) {
|
|
20
|
+
issueCreate(input: $input) {
|
|
21
|
+
success
|
|
22
|
+
issue {
|
|
23
|
+
id
|
|
24
|
+
identifier
|
|
25
|
+
title
|
|
26
|
+
url
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
exports.FILE_UPLOAD_MUTATION = `
|
|
32
|
+
mutation FileUpload($size: Int!, $contentType: String!, $filename: String!) {
|
|
33
|
+
fileUpload(size: $size, contentType: $contentType, filename: $filename) {
|
|
34
|
+
success
|
|
35
|
+
uploadFile {
|
|
36
|
+
uploadUrl
|
|
37
|
+
assetUrl
|
|
38
|
+
headers {
|
|
39
|
+
key
|
|
40
|
+
value
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
/**
|
|
47
|
+
* Request a signed upload URL from Linear's fileUpload mutation.
|
|
48
|
+
*
|
|
49
|
+
* @throws {ShakeNbakeError} on failure
|
|
50
|
+
*/
|
|
51
|
+
async function requestUploadUrl(apiKey, apiUrl, filename, contentType, size) {
|
|
52
|
+
const data = await linearFetch(apiKey, apiUrl, exports.FILE_UPLOAD_MUTATION, { size, contentType, filename });
|
|
53
|
+
if (!data.fileUpload.success) {
|
|
54
|
+
throw new core_1.ShakeNbakeError('Linear fileUpload mutation returned success=false', 'UPLOAD_FAILED', { retryable: false });
|
|
55
|
+
}
|
|
56
|
+
const { uploadUrl, assetUrl, headers } = data.fileUpload.uploadFile;
|
|
57
|
+
return { uploadUrl, assetUrl, headers };
|
|
58
|
+
}
|
|
59
|
+
// ---- Fetch Wrapper ----
|
|
60
|
+
/**
|
|
61
|
+
* Execute a GraphQL request against the Linear API.
|
|
62
|
+
*
|
|
63
|
+
* Authorization header uses the API key directly (no "Bearer" prefix),
|
|
64
|
+
* matching Linear's expected format.
|
|
65
|
+
*
|
|
66
|
+
* @throws {ShakeNbakeError} with appropriate code on failure
|
|
67
|
+
*/
|
|
68
|
+
async function linearFetch(apiKey, apiUrl, query, variables) {
|
|
69
|
+
let response;
|
|
70
|
+
try {
|
|
71
|
+
response = await fetch(apiUrl, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
Authorization: apiKey,
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({ query, variables }),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new core_1.ShakeNbakeError('Network request to Linear API failed', 'NETWORK_ERROR', { originalError: error });
|
|
82
|
+
}
|
|
83
|
+
// Handle HTTP-level errors
|
|
84
|
+
if (response.status === 401 || response.status === 403) {
|
|
85
|
+
throw new core_1.ShakeNbakeError(`Linear API authentication failed (HTTP ${String(response.status)})`, 'AUTH_FAILED', { retryable: false });
|
|
86
|
+
}
|
|
87
|
+
if (response.status === 429) {
|
|
88
|
+
throw new core_1.ShakeNbakeError('Linear API rate limit exceeded', 'RATE_LIMITED', { retryable: true });
|
|
89
|
+
}
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new core_1.ShakeNbakeError(`Linear API request failed (HTTP ${String(response.status)})`, 'UNKNOWN', { retryable: false });
|
|
92
|
+
}
|
|
93
|
+
let json;
|
|
94
|
+
try {
|
|
95
|
+
json = (await response.json());
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
throw new core_1.ShakeNbakeError('Failed to parse Linear API response', 'UNKNOWN', { originalError: error });
|
|
99
|
+
}
|
|
100
|
+
// Handle GraphQL-level errors
|
|
101
|
+
if (json.errors && json.errors.length > 0) {
|
|
102
|
+
const firstError = json.errors[0];
|
|
103
|
+
const message = firstError?.message ?? 'Unknown GraphQL error';
|
|
104
|
+
// Check if the error indicates rate limiting via extensions.code
|
|
105
|
+
const extensionCode = firstError?.extensions?.['code'];
|
|
106
|
+
if (extensionCode === 'RATELIMITED') {
|
|
107
|
+
throw new core_1.ShakeNbakeError(`Linear API rate limit exceeded: ${message}`, 'RATE_LIMITED', { retryable: true });
|
|
108
|
+
}
|
|
109
|
+
// Check if the error indicates auth failure
|
|
110
|
+
if (message.toLowerCase().includes('authentication')) {
|
|
111
|
+
throw new core_1.ShakeNbakeError(`Linear GraphQL error: ${message}`, 'AUTH_FAILED', { retryable: false });
|
|
112
|
+
}
|
|
113
|
+
throw new core_1.ShakeNbakeError(`Linear GraphQL error: ${message}`, 'UNKNOWN', { retryable: false });
|
|
114
|
+
}
|
|
115
|
+
if (!json.data) {
|
|
116
|
+
throw new core_1.ShakeNbakeError('Linear API returned empty response', 'UNKNOWN', { retryable: false });
|
|
117
|
+
}
|
|
118
|
+
return json.data;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=graphql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql.js","sourceRoot":"","sources":["../src/graphql.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { LinearAdapter } from './adapter.js';
|
|
2
|
+
export type { LinearConfig } from './types.js';
|
|
3
|
+
export { DEFAULT_SEVERITY_MAPPING, DEFAULT_API_URL } from './types.js';
|
|
4
|
+
export { linearFetch, requestUploadUrl, VIEWER_QUERY, ISSUE_CREATE_MUTATION, FILE_UPLOAD_MUTATION, } from './graphql.js';
|
|
5
|
+
export type { GraphQLResponse, ViewerData, IssueCreateData, FileUploadData, FileUploadHeader, UploadUrlResult, } from './graphql.js';
|
|
6
|
+
export { buildIssueDescription } from './markdown.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG7C,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGvE,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,eAAe,EACf,UAAU,EACV,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// @shakenbake/linear — Public API
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildIssueDescription = exports.FILE_UPLOAD_MUTATION = exports.ISSUE_CREATE_MUTATION = exports.VIEWER_QUERY = exports.requestUploadUrl = exports.linearFetch = exports.DEFAULT_API_URL = exports.DEFAULT_SEVERITY_MAPPING = exports.LinearAdapter = void 0;
|
|
7
|
+
// Adapter
|
|
8
|
+
var adapter_js_1 = require("./adapter.js");
|
|
9
|
+
Object.defineProperty(exports, "LinearAdapter", { enumerable: true, get: function () { return adapter_js_1.LinearAdapter; } });
|
|
10
|
+
var types_js_1 = require("./types.js");
|
|
11
|
+
Object.defineProperty(exports, "DEFAULT_SEVERITY_MAPPING", { enumerable: true, get: function () { return types_js_1.DEFAULT_SEVERITY_MAPPING; } });
|
|
12
|
+
Object.defineProperty(exports, "DEFAULT_API_URL", { enumerable: true, get: function () { return types_js_1.DEFAULT_API_URL; } });
|
|
13
|
+
// GraphQL (exported for advanced use / testing)
|
|
14
|
+
var graphql_js_1 = require("./graphql.js");
|
|
15
|
+
Object.defineProperty(exports, "linearFetch", { enumerable: true, get: function () { return graphql_js_1.linearFetch; } });
|
|
16
|
+
Object.defineProperty(exports, "requestUploadUrl", { enumerable: true, get: function () { return graphql_js_1.requestUploadUrl; } });
|
|
17
|
+
Object.defineProperty(exports, "VIEWER_QUERY", { enumerable: true, get: function () { return graphql_js_1.VIEWER_QUERY; } });
|
|
18
|
+
Object.defineProperty(exports, "ISSUE_CREATE_MUTATION", { enumerable: true, get: function () { return graphql_js_1.ISSUE_CREATE_MUTATION; } });
|
|
19
|
+
Object.defineProperty(exports, "FILE_UPLOAD_MUTATION", { enumerable: true, get: function () { return graphql_js_1.FILE_UPLOAD_MUTATION; } });
|
|
20
|
+
// Markdown builder (exported for customization)
|
|
21
|
+
var markdown_js_1 = require("./markdown.js");
|
|
22
|
+
Object.defineProperty(exports, "buildIssueDescription", { enumerable: true, get: function () { return markdown_js_1.buildIssueDescription; } });
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,kCAAkC;AAClC,8EAA8E;;;AAE9E,UAAU;AACV,2CAA6C;AAApC,2GAAA,aAAa,OAAA;AAItB,uCAAuE;AAA9D,oHAAA,wBAAwB,OAAA;AAAE,2GAAA,eAAe,OAAA;AAElD,gDAAgD;AAChD,2CAMsB;AALpB,yGAAA,WAAW,OAAA;AACX,8GAAA,gBAAgB,OAAA;AAChB,0GAAA,YAAY,OAAA;AACZ,mHAAA,qBAAqB,OAAA;AACrB,kHAAA,oBAAoB,OAAA;AAWtB,gDAAgD;AAChD,6CAAsD;AAA7C,oHAAA,qBAAqB,OAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BugReport } from '@shakenbake/core';
|
|
2
|
+
/**
|
|
3
|
+
* Build a formatted Markdown description for a Linear issue from a BugReport.
|
|
4
|
+
*
|
|
5
|
+
* Missing context fields are gracefully omitted (never shown as "undefined").
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildIssueDescription(report: BugReport, screenshotUrl?: string, originalScreenshotUrl?: string, audioUrl?: string): string;
|
|
8
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAiB,MAAM,kBAAkB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,EACjB,aAAa,CAAC,EAAE,MAAM,EACtB,qBAAqB,CAAC,EAAE,MAAM,EAC9B,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,CAqER"}
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// @shakenbake/linear — Markdown description builder for Linear issues
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildIssueDescription = buildIssueDescription;
|
|
7
|
+
/**
|
|
8
|
+
* Build a formatted Markdown description for a Linear issue from a BugReport.
|
|
9
|
+
*
|
|
10
|
+
* Missing context fields are gracefully omitted (never shown as "undefined").
|
|
11
|
+
*/
|
|
12
|
+
function buildIssueDescription(report, screenshotUrl, originalScreenshotUrl, audioUrl) {
|
|
13
|
+
const sections = [];
|
|
14
|
+
// Header
|
|
15
|
+
sections.push('## Bug Report');
|
|
16
|
+
sections.push('');
|
|
17
|
+
sections.push(report.description);
|
|
18
|
+
sections.push('');
|
|
19
|
+
// Audio transcript (if available)
|
|
20
|
+
if (report.audio?.transcript) {
|
|
21
|
+
sections.push('### Audio Transcript');
|
|
22
|
+
sections.push('');
|
|
23
|
+
sections.push(report.audio.transcript);
|
|
24
|
+
sections.push('');
|
|
25
|
+
}
|
|
26
|
+
// Screenshots
|
|
27
|
+
sections.push('### Screenshots');
|
|
28
|
+
sections.push('');
|
|
29
|
+
if (screenshotUrl) {
|
|
30
|
+
sections.push('**Annotated:**');
|
|
31
|
+
sections.push(``);
|
|
32
|
+
sections.push('');
|
|
33
|
+
}
|
|
34
|
+
if (originalScreenshotUrl) {
|
|
35
|
+
sections.push('**Original:**');
|
|
36
|
+
sections.push(``);
|
|
37
|
+
sections.push('');
|
|
38
|
+
}
|
|
39
|
+
// Audio attachment link
|
|
40
|
+
if (audioUrl) {
|
|
41
|
+
sections.push(`**Audio recording:** [Listen](${audioUrl})`);
|
|
42
|
+
sections.push('');
|
|
43
|
+
}
|
|
44
|
+
// Device context
|
|
45
|
+
const contextTable = buildContextTable(report.context);
|
|
46
|
+
if (contextTable) {
|
|
47
|
+
sections.push('### Device Context');
|
|
48
|
+
sections.push('');
|
|
49
|
+
sections.push('<details>');
|
|
50
|
+
sections.push('<summary>Full device and environment details</summary>');
|
|
51
|
+
sections.push('');
|
|
52
|
+
sections.push('| Field | Value |');
|
|
53
|
+
sections.push('|---|---|');
|
|
54
|
+
sections.push(contextTable);
|
|
55
|
+
sections.push('');
|
|
56
|
+
sections.push('</details>');
|
|
57
|
+
sections.push('');
|
|
58
|
+
}
|
|
59
|
+
// Console errors
|
|
60
|
+
const consoleErrors = buildConsoleErrors(report.context);
|
|
61
|
+
if (consoleErrors) {
|
|
62
|
+
sections.push('### Console Errors (last 5)');
|
|
63
|
+
sections.push('');
|
|
64
|
+
sections.push(consoleErrors);
|
|
65
|
+
sections.push('');
|
|
66
|
+
}
|
|
67
|
+
// Footer
|
|
68
|
+
sections.push('---');
|
|
69
|
+
sections.push('*Reported via [ShakeNbake](https://github.com/user/shakenbake)*');
|
|
70
|
+
return sections.join('\n');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a Markdown table of device context fields.
|
|
74
|
+
* Only includes fields that have values.
|
|
75
|
+
*/
|
|
76
|
+
function buildContextTable(context) {
|
|
77
|
+
const rows = [];
|
|
78
|
+
// Platform
|
|
79
|
+
const { platform } = context;
|
|
80
|
+
if (platform.os) {
|
|
81
|
+
const osDisplay = platform.osVersion
|
|
82
|
+
? `${platform.os} ${platform.osVersion}`
|
|
83
|
+
: platform.os;
|
|
84
|
+
rows.push(`| Platform | ${osDisplay} |`);
|
|
85
|
+
}
|
|
86
|
+
if (platform.userAgent) {
|
|
87
|
+
rows.push(`| User Agent | ${platform.userAgent} |`);
|
|
88
|
+
}
|
|
89
|
+
if (platform.browser) {
|
|
90
|
+
rows.push(`| Browser | ${platform.browser} |`);
|
|
91
|
+
}
|
|
92
|
+
// Device
|
|
93
|
+
const { device } = context;
|
|
94
|
+
const deviceParts = [device.manufacturer, device.model].filter(Boolean);
|
|
95
|
+
if (deviceParts.length > 0) {
|
|
96
|
+
rows.push(`| Device | ${deviceParts.join(' ')} |`);
|
|
97
|
+
}
|
|
98
|
+
// Screen
|
|
99
|
+
const { screen } = context;
|
|
100
|
+
if (screen.width && screen.height) {
|
|
101
|
+
const scaleStr = screen.scale ? ` @${String(screen.scale)}x` : '';
|
|
102
|
+
rows.push(`| Screen | ${String(screen.width)}x${String(screen.height)}${scaleStr} |`);
|
|
103
|
+
}
|
|
104
|
+
// Network
|
|
105
|
+
const { network } = context;
|
|
106
|
+
const networkParts = [];
|
|
107
|
+
if (network.type)
|
|
108
|
+
networkParts.push(network.type);
|
|
109
|
+
if (network.effectiveType)
|
|
110
|
+
networkParts.push(`(${network.effectiveType})`);
|
|
111
|
+
if (networkParts.length > 0) {
|
|
112
|
+
rows.push(`| Network | ${networkParts.join(' ')} |`);
|
|
113
|
+
}
|
|
114
|
+
// Battery
|
|
115
|
+
const { battery } = context;
|
|
116
|
+
if (battery.level !== undefined) {
|
|
117
|
+
const stateStr = battery.state ? ` (${battery.state})` : '';
|
|
118
|
+
rows.push(`| Battery | ${String(battery.level)}%${stateStr} |`);
|
|
119
|
+
}
|
|
120
|
+
// Locale
|
|
121
|
+
const { locale } = context;
|
|
122
|
+
const localeParts = [locale.languageCode, locale.regionCode].filter(Boolean);
|
|
123
|
+
if (localeParts.length > 0) {
|
|
124
|
+
rows.push(`| Locale | ${localeParts.join('-')} |`);
|
|
125
|
+
}
|
|
126
|
+
if (locale.timezone) {
|
|
127
|
+
rows.push(`| Timezone | ${locale.timezone} |`);
|
|
128
|
+
}
|
|
129
|
+
// App
|
|
130
|
+
const { app } = context;
|
|
131
|
+
if (app.version) {
|
|
132
|
+
const buildStr = app.buildNumber ? ` (${app.buildNumber})` : '';
|
|
133
|
+
rows.push(`| App Version | ${app.version}${buildStr} |`);
|
|
134
|
+
}
|
|
135
|
+
if (app.url) {
|
|
136
|
+
rows.push(`| URL | ${app.url} |`);
|
|
137
|
+
}
|
|
138
|
+
// Navigation
|
|
139
|
+
const { navigation } = context;
|
|
140
|
+
if (navigation.currentRoute) {
|
|
141
|
+
rows.push(`| Current Route | ${navigation.currentRoute} |`);
|
|
142
|
+
}
|
|
143
|
+
return rows.join('\n');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build a Markdown section for console errors (last 5).
|
|
147
|
+
*/
|
|
148
|
+
function buildConsoleErrors(context) {
|
|
149
|
+
const errors = context.console.recentErrors;
|
|
150
|
+
if (!errors || errors.length === 0) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const last5 = errors.slice(-5);
|
|
154
|
+
const formatted = last5.map((err) => {
|
|
155
|
+
const lines = [`\`\`\``, err.message];
|
|
156
|
+
if (err.stack) {
|
|
157
|
+
lines.push(err.stack);
|
|
158
|
+
}
|
|
159
|
+
lines.push(`\`\`\``);
|
|
160
|
+
lines.push(`_${err.timestamp}_`);
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
});
|
|
163
|
+
return formatted.join('\n\n');
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=markdown.js.map
|