@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.
@@ -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"}
@@ -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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"}
@@ -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"}
@@ -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(`![Annotated screenshot](${screenshotUrl})`);
32
+ sections.push('');
33
+ }
34
+ if (originalScreenshotUrl) {
35
+ sections.push('**Original:**');
36
+ sections.push(`![Original screenshot](${originalScreenshotUrl})`);
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