@stackmemoryai/stackmemory 0.5.29 → 0.5.31
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 +53 -32
- package/dist/core/database/batch-operations.js +29 -4
- package/dist/core/database/batch-operations.js.map +2 -2
- package/dist/core/database/connection-pool.js +13 -2
- package/dist/core/database/connection-pool.js.map +2 -2
- package/dist/core/database/migration-manager.js +130 -34
- package/dist/core/database/migration-manager.js.map +2 -2
- package/dist/core/database/paradedb-adapter.js +23 -7
- package/dist/core/database/paradedb-adapter.js.map +2 -2
- package/dist/core/database/query-router.js +8 -3
- package/dist/core/database/query-router.js.map +2 -2
- package/dist/core/database/sqlite-adapter.js +152 -33
- package/dist/core/database/sqlite-adapter.js.map +2 -2
- package/dist/integrations/linear/auth.js +34 -20
- package/dist/integrations/linear/auth.js.map +2 -2
- package/dist/integrations/linear/auto-sync.js +18 -8
- package/dist/integrations/linear/auto-sync.js.map +2 -2
- package/dist/integrations/linear/client.js +42 -9
- package/dist/integrations/linear/client.js.map +2 -2
- package/dist/integrations/linear/migration.js +94 -36
- package/dist/integrations/linear/migration.js.map +2 -2
- package/dist/integrations/linear/oauth-server.js +77 -34
- package/dist/integrations/linear/oauth-server.js.map +2 -2
- package/dist/integrations/linear/rest-client.js +13 -3
- package/dist/integrations/linear/rest-client.js.map +2 -2
- package/dist/integrations/linear/sync-service.js +18 -15
- package/dist/integrations/linear/sync-service.js.map +2 -2
- package/dist/integrations/linear/sync.js +12 -4
- package/dist/integrations/linear/sync.js.map +2 -2
- package/dist/integrations/linear/unified-sync.js +33 -8
- package/dist/integrations/linear/unified-sync.js.map +2 -2
- package/dist/integrations/linear/webhook-handler.js +5 -1
- package/dist/integrations/linear/webhook-handler.js.map +2 -2
- package/dist/integrations/linear/webhook-server.js +7 -7
- package/dist/integrations/linear/webhook-server.js.map +2 -2
- package/dist/integrations/linear/webhook.js +9 -2
- package/dist/integrations/linear/webhook.js.map +2 -2
- package/dist/integrations/mcp/schemas.js +147 -0
- package/dist/integrations/mcp/schemas.js.map +7 -0
- package/dist/integrations/mcp/server.js +19 -3
- package/dist/integrations/mcp/server.js.map +2 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ const __filename = __fileURLToPath(import.meta.url);
|
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { LinearRestClient } from "./rest-client.js";
|
|
6
6
|
import { logger } from "../../core/monitoring/logger.js";
|
|
7
|
+
import { IntegrationError, ErrorCode } from "../../core/errors/index.js";
|
|
7
8
|
import chalk from "chalk";
|
|
8
9
|
class LinearMigrator {
|
|
9
10
|
sourceClient;
|
|
@@ -71,26 +72,37 @@ class LinearMigrator {
|
|
|
71
72
|
taskMappings: []
|
|
72
73
|
};
|
|
73
74
|
try {
|
|
74
|
-
console.log(chalk.yellow("
|
|
75
|
+
console.log(chalk.yellow("Starting Linear workspace migration..."));
|
|
75
76
|
const sourceTasks = await this.sourceClient.getAllTasks(true);
|
|
76
77
|
result.totalTasks = sourceTasks.length;
|
|
77
|
-
console.log(
|
|
78
|
+
console.log(
|
|
79
|
+
chalk.cyan(`Found ${sourceTasks.length} tasks in source workspace`)
|
|
80
|
+
);
|
|
78
81
|
let tasksToMigrate = sourceTasks;
|
|
79
82
|
if (this.config.taskPrefix) {
|
|
80
83
|
tasksToMigrate = sourceTasks.filter(
|
|
81
84
|
(task) => task.identifier.startsWith(this.config.taskPrefix)
|
|
82
85
|
);
|
|
83
|
-
console.log(
|
|
86
|
+
console.log(
|
|
87
|
+
chalk.cyan(
|
|
88
|
+
`Filtered to ${tasksToMigrate.length} tasks with prefix "${this.config.taskPrefix}"`
|
|
89
|
+
)
|
|
90
|
+
);
|
|
84
91
|
}
|
|
85
92
|
if (this.config.includeStates?.length) {
|
|
86
93
|
tasksToMigrate = tasksToMigrate.filter(
|
|
87
94
|
(task) => this.config.includeStates.includes(task.state.type)
|
|
88
95
|
);
|
|
89
|
-
|
|
96
|
+
const stateStr = this.config.includeStates.join(", ");
|
|
97
|
+
console.log(
|
|
98
|
+
chalk.cyan(
|
|
99
|
+
`Further filtered to ${tasksToMigrate.length} tasks matching states: ${stateStr}`
|
|
100
|
+
)
|
|
101
|
+
);
|
|
90
102
|
}
|
|
91
103
|
result.exported = tasksToMigrate.length;
|
|
92
104
|
if (this.config.dryRun) {
|
|
93
|
-
console.log(chalk.yellow("
|
|
105
|
+
console.log(chalk.yellow("DRY RUN - No tasks will be created"));
|
|
94
106
|
tasksToMigrate.forEach((task) => {
|
|
95
107
|
result.taskMappings.push({
|
|
96
108
|
sourceId: task.id,
|
|
@@ -103,12 +115,18 @@ class LinearMigrator {
|
|
|
103
115
|
return result;
|
|
104
116
|
}
|
|
105
117
|
const targetTeam = await this.targetClient.getTeam();
|
|
106
|
-
console.log(
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.cyan(`Target team: ${targetTeam.name} (${targetTeam.key})`)
|
|
120
|
+
);
|
|
107
121
|
const batchSize = this.config.batchSize || 5;
|
|
108
122
|
const delayMs = this.config.delayMs || 2e3;
|
|
109
123
|
for (let i = 0; i < tasksToMigrate.length; i += batchSize) {
|
|
110
124
|
const batch = tasksToMigrate.slice(i, i + batchSize);
|
|
111
|
-
console.log(
|
|
125
|
+
console.log(
|
|
126
|
+
chalk.yellow(
|
|
127
|
+
`Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(tasksToMigrate.length / batchSize)}`
|
|
128
|
+
)
|
|
129
|
+
);
|
|
112
130
|
for (const task of batch) {
|
|
113
131
|
try {
|
|
114
132
|
const newTask = await this.migrateTask(task, targetTeam.id);
|
|
@@ -120,17 +138,29 @@ class LinearMigrator {
|
|
|
120
138
|
deleted: false
|
|
121
139
|
};
|
|
122
140
|
result.imported++;
|
|
123
|
-
console.log(
|
|
141
|
+
console.log(
|
|
142
|
+
chalk.green(
|
|
143
|
+
`${task.identifier} \u2192 ${newTask.identifier}: ${task.title}`
|
|
144
|
+
)
|
|
145
|
+
);
|
|
124
146
|
if (this.config.deleteFromSource) {
|
|
125
147
|
try {
|
|
126
148
|
await this.deleteTask(task.id);
|
|
127
149
|
mapping.deleted = true;
|
|
128
150
|
result.deleted++;
|
|
129
|
-
console.log(
|
|
151
|
+
console.log(
|
|
152
|
+
chalk.gray(`Deleted ${task.identifier} from source`)
|
|
153
|
+
);
|
|
130
154
|
} catch (deleteError) {
|
|
131
155
|
result.deleteFailed++;
|
|
132
|
-
result.errors.push(
|
|
133
|
-
|
|
156
|
+
result.errors.push(
|
|
157
|
+
`Delete failed for ${task.identifier}: ${deleteError.message}`
|
|
158
|
+
);
|
|
159
|
+
console.log(
|
|
160
|
+
chalk.yellow(
|
|
161
|
+
`Failed to delete ${task.identifier} from source: ${deleteError.message}`
|
|
162
|
+
)
|
|
163
|
+
);
|
|
134
164
|
}
|
|
135
165
|
}
|
|
136
166
|
result.taskMappings.push(mapping);
|
|
@@ -143,11 +173,11 @@ class LinearMigrator {
|
|
|
143
173
|
error: errorMsg
|
|
144
174
|
});
|
|
145
175
|
result.failed++;
|
|
146
|
-
console.log(chalk.red(
|
|
176
|
+
console.log(chalk.red(`${task.identifier}: ${errorMsg}`));
|
|
147
177
|
}
|
|
148
178
|
}
|
|
149
179
|
if (i + batchSize < tasksToMigrate.length) {
|
|
150
|
-
console.log(chalk.gray(
|
|
180
|
+
console.log(chalk.gray(`Waiting ${delayMs}ms before next batch...`));
|
|
151
181
|
await this.delay(delayMs);
|
|
152
182
|
}
|
|
153
183
|
}
|
|
@@ -162,11 +192,11 @@ class LinearMigrator {
|
|
|
162
192
|
*/
|
|
163
193
|
async migrateTask(sourceTask, targetTeamId) {
|
|
164
194
|
const stateMapping = {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
195
|
+
backlog: "backlog",
|
|
196
|
+
unstarted: "unstarted",
|
|
197
|
+
started: "started",
|
|
198
|
+
completed: "completed",
|
|
199
|
+
canceled: "canceled"
|
|
170
200
|
};
|
|
171
201
|
const createTaskQuery = `
|
|
172
202
|
mutation CreateIssue($input: IssueCreateInput!) {
|
|
@@ -198,7 +228,11 @@ class LinearMigrator {
|
|
|
198
228
|
};
|
|
199
229
|
const response = await this.targetClient.makeRequest(createTaskQuery, { input: taskInput });
|
|
200
230
|
if (!response.data?.issueCreate?.success) {
|
|
201
|
-
throw new
|
|
231
|
+
throw new IntegrationError(
|
|
232
|
+
"Failed to create task in target workspace",
|
|
233
|
+
ErrorCode.LINEAR_SYNC_FAILED,
|
|
234
|
+
{ sourceTaskId: sourceTask.id, sourceIdentifier: sourceTask.identifier }
|
|
235
|
+
);
|
|
202
236
|
}
|
|
203
237
|
return response.data.issueCreate.issue;
|
|
204
238
|
}
|
|
@@ -245,9 +279,15 @@ class LinearMigrator {
|
|
|
245
279
|
}
|
|
246
280
|
}
|
|
247
281
|
`;
|
|
248
|
-
const response = await this.sourceClient.makeRequest(deleteQuery, {
|
|
282
|
+
const response = await this.sourceClient.makeRequest(deleteQuery, {
|
|
283
|
+
id: taskId
|
|
284
|
+
});
|
|
249
285
|
if (!response.data?.issueDelete?.success) {
|
|
250
|
-
throw new
|
|
286
|
+
throw new IntegrationError(
|
|
287
|
+
"Failed to delete task from source workspace",
|
|
288
|
+
ErrorCode.LINEAR_SYNC_FAILED,
|
|
289
|
+
{ taskId }
|
|
290
|
+
);
|
|
251
291
|
}
|
|
252
292
|
}
|
|
253
293
|
/**
|
|
@@ -259,40 +299,58 @@ class LinearMigrator {
|
|
|
259
299
|
}
|
|
260
300
|
async function runMigration(config) {
|
|
261
301
|
const migrator = new LinearMigrator(config);
|
|
262
|
-
console.log(chalk.blue("
|
|
302
|
+
console.log(chalk.blue("Testing connections..."));
|
|
263
303
|
const connectionTest = await migrator.testConnections();
|
|
264
304
|
if (!connectionTest.source.success) {
|
|
265
|
-
console.error(
|
|
305
|
+
console.error(
|
|
306
|
+
chalk.red(`Source connection failed: ${connectionTest.source.error}`)
|
|
307
|
+
);
|
|
266
308
|
return;
|
|
267
309
|
}
|
|
268
310
|
if (!connectionTest.target.success) {
|
|
269
|
-
console.error(
|
|
311
|
+
console.error(
|
|
312
|
+
chalk.red(`Target connection failed: ${connectionTest.target.error}`)
|
|
313
|
+
);
|
|
270
314
|
return;
|
|
271
315
|
}
|
|
272
|
-
console.log(chalk.green("
|
|
273
|
-
console.log(
|
|
274
|
-
|
|
316
|
+
console.log(chalk.green("Both connections successful"));
|
|
317
|
+
console.log(
|
|
318
|
+
chalk.cyan(
|
|
319
|
+
`Source: ${connectionTest.source.info.user.name} @ ${connectionTest.source.info.team.name}`
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
console.log(
|
|
323
|
+
chalk.cyan(
|
|
324
|
+
`Target: ${connectionTest.target.info.user.name} @ ${connectionTest.target.info.team.name}`
|
|
325
|
+
)
|
|
326
|
+
);
|
|
275
327
|
const result = await migrator.migrate();
|
|
276
|
-
console.log(chalk.blue("\
|
|
328
|
+
console.log(chalk.blue("\nMigration Summary:"));
|
|
277
329
|
console.log(` Total tasks: ${result.totalTasks}`);
|
|
278
330
|
console.log(` Exported: ${result.exported}`);
|
|
279
|
-
console.log(chalk.green(`
|
|
280
|
-
console.log(chalk.red(`
|
|
331
|
+
console.log(chalk.green(` Imported: ${result.imported}`));
|
|
332
|
+
console.log(chalk.red(` Failed: ${result.failed}`));
|
|
281
333
|
if (config.deleteFromSource) {
|
|
282
|
-
console.log(chalk.gray(`
|
|
334
|
+
console.log(chalk.gray(` Deleted: ${result.deleted}`));
|
|
283
335
|
if (result.deleteFailed > 0) {
|
|
284
|
-
console.log(chalk.yellow(`
|
|
336
|
+
console.log(chalk.yellow(` Delete failed: ${result.deleteFailed}`));
|
|
285
337
|
}
|
|
286
338
|
}
|
|
287
339
|
if (result.errors.length > 0) {
|
|
288
|
-
console.log(chalk.red("\
|
|
340
|
+
console.log(chalk.red("\nErrors:"));
|
|
289
341
|
result.errors.forEach((error) => console.log(chalk.red(` - ${error}`)));
|
|
290
342
|
}
|
|
291
343
|
if (result.imported > 0) {
|
|
292
|
-
console.log(
|
|
293
|
-
|
|
344
|
+
console.log(
|
|
345
|
+
chalk.green(
|
|
346
|
+
`
|
|
347
|
+
Migration completed! ${result.imported} tasks migrated successfully.`
|
|
348
|
+
)
|
|
349
|
+
);
|
|
294
350
|
if (config.deleteFromSource && result.deleted > 0) {
|
|
295
|
-
console.log(
|
|
351
|
+
console.log(
|
|
352
|
+
chalk.gray(` ${result.deleted} tasks deleted from source workspace.`)
|
|
353
|
+
);
|
|
296
354
|
}
|
|
297
355
|
}
|
|
298
356
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/integrations/linear/migration.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Linear Workspace Migration Tool\n * Migrates all tasks from one Linear workspace to another\n */\n\nimport { LinearRestClient } from './rest-client.js';\nimport { logger } from '../../core/monitoring/logger.js';\nimport chalk from 'chalk';\n\nexport interface MigrationConfig {\n sourceApiKey: string;\n targetApiKey: string;\n dryRun?: boolean;\n includeStates?: string[]; // Filter by state\n taskPrefix?: string; // Only migrate tasks with this identifier prefix (e.g., \"STA-\")\n deleteFromSource?: boolean; // Delete tasks from source after successful migration\n batchSize?: number;\n delayMs?: number; // Delay between API calls\n}\n\nexport interface MigrationResult {\n totalTasks: number;\n exported: number;\n imported: number;\n failed: number;\n deleted: number;\n deleteFailed: number;\n errors: string[];\n taskMappings: Array<{\n sourceId: string;\n sourceIdentifier: string;\n targetId?: string;\n targetIdentifier?: string;\n deleted?: boolean;\n error?: string;\n }>;\n}\n\nexport class LinearMigrator {\n private sourceClient: LinearRestClient;\n private targetClient: LinearRestClient;\n private config: MigrationConfig;\n\n constructor(config: MigrationConfig) {\n this.config = config;\n this.sourceClient = new LinearRestClient(config.sourceApiKey);\n this.targetClient = new LinearRestClient(config.targetApiKey);\n }\n\n /**\n * Test connections to both workspaces\n */\n async testConnections(): Promise<{\n source: { success: boolean; info?: any; error?: string };\n target: { success: boolean; info?: any; error?: string };\n }> {\n const result = {\n source: { success: false } as any,\n target: { success: false } as any\n };\n\n // Test source connection\n try {\n const sourceViewer = await this.sourceClient.getViewer();\n const sourceTeam = await this.sourceClient.getTeam();\n result.source = {\n success: true,\n info: {\n user: sourceViewer,\n team: sourceTeam\n }\n };\n } catch (error: unknown) {\n result.source = {\n success: false,\n error: (error as Error).message\n };\n }\n\n // Test target connection \n try {\n const targetViewer = await this.targetClient.getViewer();\n const targetTeam = await this.targetClient.getTeam();\n result.target = {\n success: true,\n info: {\n user: targetViewer,\n team: targetTeam\n }\n };\n } catch (error: unknown) {\n result.target = {\n success: false,\n error: (error as Error).message\n };\n }\n\n return result;\n }\n\n /**\n * Migrate all tasks from source to target workspace\n */\n async migrate(): Promise<MigrationResult> {\n const result: MigrationResult = {\n totalTasks: 0,\n exported: 0,\n imported: 0,\n failed: 0,\n deleted: 0,\n deleteFailed: 0,\n errors: [],\n taskMappings: []\n };\n\n try {\n console.log(chalk.yellow('\uD83D\uDD04 Starting Linear workspace migration...'));\n\n // Get all tasks from source\n const sourceTasks = await this.sourceClient.getAllTasks(true); // Force refresh\n result.totalTasks = sourceTasks.length;\n console.log(chalk.cyan(`\uD83D\uDCCB Found ${sourceTasks.length} tasks in source workspace`));\n\n // Filter by prefix (e.g., \"STA-\" tasks only)\n let tasksToMigrate = sourceTasks;\n if (this.config.taskPrefix) {\n tasksToMigrate = sourceTasks.filter((task: any) => \n task.identifier.startsWith(this.config.taskPrefix!)\n );\n console.log(chalk.cyan(`\uD83D\uDCCB Filtered to ${tasksToMigrate.length} tasks with prefix \"${this.config.taskPrefix}\"`));\n }\n\n // Filter by states if specified\n if (this.config.includeStates?.length) {\n tasksToMigrate = tasksToMigrate.filter((task: any) => \n this.config.includeStates!.includes(task.state.type)\n );\n console.log(chalk.cyan(`\uD83D\uDCCB Further filtered to ${tasksToMigrate.length} tasks matching states: ${this.config.includeStates.join(', ')}`));\n }\n\n result.exported = tasksToMigrate.length;\n\n if (this.config.dryRun) {\n console.log(chalk.yellow('\uD83D\uDD0D DRY RUN - No tasks will be created'));\n tasksToMigrate.forEach(task => {\n result.taskMappings.push({\n sourceId: task.id,\n sourceIdentifier: task.identifier,\n targetId: 'DRY_RUN',\n targetIdentifier: 'DRY_RUN'\n });\n });\n result.imported = tasksToMigrate.length;\n return result;\n }\n\n // Get target team info\n const targetTeam = await this.targetClient.getTeam();\n console.log(chalk.cyan(`\uD83C\uDFAF Target team: ${targetTeam.name} (${targetTeam.key})`));\n\n // Migrate tasks in batches\n const batchSize = this.config.batchSize || 5;\n const delayMs = this.config.delayMs || 2000;\n\n for (let i = 0; i < tasksToMigrate.length; i += batchSize) {\n const batch = tasksToMigrate.slice(i, i + batchSize);\n console.log(chalk.yellow(`\uD83D\uDCE6 Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(tasksToMigrate.length / batchSize)}`));\n\n for (const task of batch) {\n try {\n const newTask = await this.migrateTask(task, targetTeam.id);\n const mapping = {\n sourceId: task.id,\n sourceIdentifier: task.identifier,\n targetId: newTask.id,\n targetIdentifier: newTask.identifier,\n deleted: false\n };\n\n result.imported++;\n console.log(chalk.green(`\u2705 ${task.identifier} \u2192 ${newTask.identifier}: ${task.title}`));\n\n // Delete from source if configured\n if (this.config.deleteFromSource) {\n try {\n await this.deleteTask(task.id);\n mapping.deleted = true;\n result.deleted++;\n console.log(chalk.gray(`\uD83D\uDDD1\uFE0F Deleted ${task.identifier} from source`));\n } catch (deleteError: unknown) {\n result.deleteFailed++;\n result.errors.push(`Delete failed for ${task.identifier}: ${(deleteError as Error).message}`);\n console.log(chalk.yellow(`\u26A0\uFE0F Failed to delete ${task.identifier} from source: ${(deleteError as Error).message}`));\n }\n }\n\n result.taskMappings.push(mapping);\n } catch (error: unknown) {\n const errorMsg = (error as Error).message;\n result.errors.push(`${task.identifier}: ${errorMsg}`);\n result.taskMappings.push({\n sourceId: task.id,\n sourceIdentifier: task.identifier,\n error: errorMsg\n });\n result.failed++;\n console.log(chalk.red(`\u274C ${task.identifier}: ${errorMsg}`));\n }\n }\n\n // Delay between batches to avoid rate limits\n if (i + batchSize < tasksToMigrate.length) {\n console.log(chalk.gray(`\u23F3 Waiting ${delayMs}ms before next batch...`));\n await this.delay(delayMs);\n }\n }\n\n } catch (error: unknown) {\n result.errors.push(`Migration failed: ${(error as Error).message}`);\n logger.error('Migration failed:', error as Error);\n }\n\n return result;\n }\n\n /**\n * Migrate a single task\n */\n private async migrateTask(sourceTask: any, targetTeamId: string): Promise<any> {\n // Map states from source to target format\n const stateMapping: Record<string, string> = {\n 'backlog': 'backlog',\n 'unstarted': 'unstarted', \n 'started': 'started',\n 'completed': 'completed',\n 'canceled': 'canceled'\n };\n\n // Create task in target workspace using GraphQL\n const createTaskQuery = `\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n // Prepare task input\n const taskInput = {\n title: `[MIGRATED] ${sourceTask.title}`,\n description: this.formatMigratedDescription(sourceTask),\n teamId: targetTeamId,\n priority: this.mapPriority(sourceTask.priority)\n };\n\n const response = await this.targetClient.makeRequest<{\n data: {\n issueCreate: {\n success: boolean;\n issue: any;\n };\n };\n }>(createTaskQuery, { input: taskInput });\n\n if (!response.data?.issueCreate?.success) {\n throw new Error('Failed to create task in target workspace');\n }\n\n return response.data.issueCreate.issue;\n }\n\n /**\n * Format description with migration context\n */\n private formatMigratedDescription(sourceTask: any): string {\n let description = sourceTask.description || '';\n \n description += `\\n\\n---\\n**Migration Info:**\\n`;\n description += `- Original ID: ${sourceTask.identifier}\\n`;\n description += `- Migrated: ${new Date().toISOString()}\\n`;\n description += `- Original State: ${sourceTask.state.name}\\n`;\n \n if (sourceTask.assignee) {\n description += `- Original Assignee: ${sourceTask.assignee.name}\\n`;\n }\n \n if (sourceTask.estimate) {\n description += `- Original Estimate: ${sourceTask.estimate} points\\n`;\n }\n\n return description;\n }\n\n /**\n * Map priority values\n */\n private mapPriority(priority?: number): number {\n // Linear priorities: 0=none, 1=urgent, 2=high, 3=medium, 4=low\n return priority || 0;\n }\n\n /**\n * Delete a task from the source workspace\n */\n private async deleteTask(taskId: string): Promise<void> {\n const deleteQuery = `\n mutation DeleteIssue($id: String!) {\n issueDelete(id: $id) {\n success\n }\n }\n `;\n\n const response = await (this.sourceClient as any).makeRequest(deleteQuery, { id: taskId });\n \n if (!response.data?.issueDelete?.success) {\n throw new Error('Failed to delete task from source workspace');\n }\n }\n\n /**\n * Delay helper\n */\n private delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n}\n\n/**\n * CLI function to run migration\n */\nexport async function runMigration(config: MigrationConfig): Promise<void> {\n const migrator = new LinearMigrator(config);\n \n console.log(chalk.blue('\uD83D\uDD0D Testing connections...'));\n const connectionTest = await migrator.testConnections();\n \n if (!connectionTest.source.success) {\n console.error(chalk.red(`\u274C Source connection failed: ${connectionTest.source.error}`));\n return;\n }\n \n if (!connectionTest.target.success) {\n console.error(chalk.red(`\u274C Target connection failed: ${connectionTest.target.error}`));\n return;\n }\n \n console.log(chalk.green('\u2705 Both connections successful'));\n console.log(chalk.cyan(`\uD83D\uDCE4 Source: ${connectionTest.source.info.user.name} @ ${connectionTest.source.info.team.name}`));\n console.log(chalk.cyan(`\uD83D\uDCE5 Target: ${connectionTest.target.info.user.name} @ ${connectionTest.target.info.team.name}`));\n \n const result = await migrator.migrate();\n \n console.log(chalk.blue('\\n\uD83D\uDCCA Migration Summary:'));\n console.log(` Total tasks: ${result.totalTasks}`);\n console.log(` Exported: ${result.exported}`);\n console.log(chalk.green(` \u2705 Imported: ${result.imported}`));\n console.log(chalk.red(` \u274C Failed: ${result.failed}`));\n if (config.deleteFromSource) {\n console.log(chalk.gray(` \uD83D\uDDD1\uFE0F Deleted: ${result.deleted}`));\n if (result.deleteFailed > 0) {\n console.log(chalk.yellow(` \u26A0\uFE0F Delete failed: ${result.deleteFailed}`));\n }\n }\n \n if (result.errors.length > 0) {\n console.log(chalk.red('\\n\u274C Errors:'));\n result.errors.forEach(error => console.log(chalk.red(` - ${error}`)));\n }\n \n if (result.imported > 0) {\n console.log(chalk.green(`\\n\uD83C\uDF89 Migration completed! ${result.imported} tasks migrated successfully.`));\n if (config.deleteFromSource && result.deleted > 0) {\n console.log(chalk.gray(` ${result.deleted} tasks deleted from source workspace.`));\n }\n }\n}"],
|
|
5
|
-
"mappings": ";;;;AAKA,SAAS,wBAAwB;AACjC,SAAS,cAAc;AACvB,OAAO,WAAW;AA+BX,MAAM,eAAe;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,QAAyB;AACnC,SAAK,SAAS;AACd,SAAK,eAAe,IAAI,iBAAiB,OAAO,YAAY;AAC5D,SAAK,eAAe,IAAI,iBAAiB,OAAO,YAAY;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,kBAGH;AACD,UAAM,SAAS;AAAA,MACb,QAAQ,EAAE,SAAS,MAAM;AAAA,MACzB,QAAQ,EAAE,SAAS,MAAM;AAAA,IAC3B;AAGA,QAAI;AACF,YAAM,eAAe,MAAM,KAAK,aAAa,UAAU;AACvD,YAAM,aAAa,MAAM,KAAK,aAAa,QAAQ;AACnD,aAAO,SAAS;AAAA,QACd,SAAS;AAAA,QACT,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,MAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF,SAAS,OAAgB;AACvB,aAAO,SAAS;AAAA,QACd,SAAS;AAAA,QACT,OAAQ,MAAgB;AAAA,MAC1B;AAAA,IACF;AAGA,QAAI;AACF,YAAM,eAAe,MAAM,KAAK,aAAa,UAAU;AACvD,YAAM,aAAa,MAAM,KAAK,aAAa,QAAQ;AACnD,aAAO,SAAS;AAAA,QACd,SAAS;AAAA,QACT,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,MAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF,SAAS,OAAgB;AACvB,aAAO,SAAS;AAAA,QACd,SAAS;AAAA,QACT,OAAQ,MAAgB;AAAA,MAC1B;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAoC;AACxC,UAAM,SAA0B;AAAA,MAC9B,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,cAAc;AAAA,MACd,QAAQ,CAAC;AAAA,MACT,cAAc,CAAC;AAAA,IACjB;AAEA,QAAI;AACF,cAAQ,IAAI,MAAM,OAAO,
|
|
4
|
+
"sourcesContent": ["/**\n * Linear Workspace Migration Tool\n * Migrates all tasks from one Linear workspace to another\n */\n\nimport { LinearRestClient } from './rest-client.js';\nimport { logger } from '../../core/monitoring/logger.js';\nimport { IntegrationError, ErrorCode } from '../../core/errors/index.js';\nimport chalk from 'chalk';\n\nexport interface MigrationConfig {\n sourceApiKey: string;\n targetApiKey: string;\n dryRun?: boolean;\n includeStates?: string[]; // Filter by state\n taskPrefix?: string; // Only migrate tasks with this identifier prefix (e.g., \"STA-\")\n deleteFromSource?: boolean; // Delete tasks from source after successful migration\n batchSize?: number;\n delayMs?: number; // Delay between API calls\n}\n\nexport interface MigrationResult {\n totalTasks: number;\n exported: number;\n imported: number;\n failed: number;\n deleted: number;\n deleteFailed: number;\n errors: string[];\n taskMappings: Array<{\n sourceId: string;\n sourceIdentifier: string;\n targetId?: string;\n targetIdentifier?: string;\n deleted?: boolean;\n error?: string;\n }>;\n}\n\nexport class LinearMigrator {\n private sourceClient: LinearRestClient;\n private targetClient: LinearRestClient;\n private config: MigrationConfig;\n\n constructor(config: MigrationConfig) {\n this.config = config;\n this.sourceClient = new LinearRestClient(config.sourceApiKey);\n this.targetClient = new LinearRestClient(config.targetApiKey);\n }\n\n /**\n * Test connections to both workspaces\n */\n async testConnections(): Promise<{\n source: { success: boolean; info?: any; error?: string };\n target: { success: boolean; info?: any; error?: string };\n }> {\n const result = {\n source: { success: false } as any,\n target: { success: false } as any,\n };\n\n // Test source connection\n try {\n const sourceViewer = await this.sourceClient.getViewer();\n const sourceTeam = await this.sourceClient.getTeam();\n result.source = {\n success: true,\n info: {\n user: sourceViewer,\n team: sourceTeam,\n },\n };\n } catch (error: unknown) {\n result.source = {\n success: false,\n error: (error as Error).message,\n };\n }\n\n // Test target connection\n try {\n const targetViewer = await this.targetClient.getViewer();\n const targetTeam = await this.targetClient.getTeam();\n result.target = {\n success: true,\n info: {\n user: targetViewer,\n team: targetTeam,\n },\n };\n } catch (error: unknown) {\n result.target = {\n success: false,\n error: (error as Error).message,\n };\n }\n\n return result;\n }\n\n /**\n * Migrate all tasks from source to target workspace\n */\n async migrate(): Promise<MigrationResult> {\n const result: MigrationResult = {\n totalTasks: 0,\n exported: 0,\n imported: 0,\n failed: 0,\n deleted: 0,\n deleteFailed: 0,\n errors: [],\n taskMappings: [],\n };\n\n try {\n console.log(chalk.yellow('Starting Linear workspace migration...'));\n\n // Get all tasks from source\n const sourceTasks = await this.sourceClient.getAllTasks(true); // Force refresh\n result.totalTasks = sourceTasks.length;\n console.log(\n chalk.cyan(`Found ${sourceTasks.length} tasks in source workspace`)\n );\n\n // Filter by prefix (e.g., \"STA-\" tasks only)\n let tasksToMigrate = sourceTasks;\n if (this.config.taskPrefix) {\n tasksToMigrate = sourceTasks.filter((task: any) =>\n task.identifier.startsWith(this.config.taskPrefix!)\n );\n console.log(\n chalk.cyan(\n `Filtered to ${tasksToMigrate.length} tasks with prefix \"${this.config.taskPrefix}\"`\n )\n );\n }\n\n // Filter by states if specified\n if (this.config.includeStates?.length) {\n tasksToMigrate = tasksToMigrate.filter((task: any) =>\n this.config.includeStates!.includes(task.state.type)\n );\n const stateStr = this.config.includeStates.join(', ');\n console.log(\n chalk.cyan(\n `Further filtered to ${tasksToMigrate.length} tasks matching states: ${stateStr}`\n )\n );\n }\n\n result.exported = tasksToMigrate.length;\n\n if (this.config.dryRun) {\n console.log(chalk.yellow('DRY RUN - No tasks will be created'));\n tasksToMigrate.forEach((task) => {\n result.taskMappings.push({\n sourceId: task.id,\n sourceIdentifier: task.identifier,\n targetId: 'DRY_RUN',\n targetIdentifier: 'DRY_RUN',\n });\n });\n result.imported = tasksToMigrate.length;\n return result;\n }\n\n // Get target team info\n const targetTeam = await this.targetClient.getTeam();\n console.log(\n chalk.cyan(`Target team: ${targetTeam.name} (${targetTeam.key})`)\n );\n\n // Migrate tasks in batches\n const batchSize = this.config.batchSize || 5;\n const delayMs = this.config.delayMs || 2000;\n\n for (let i = 0; i < tasksToMigrate.length; i += batchSize) {\n const batch = tasksToMigrate.slice(i, i + batchSize);\n console.log(\n chalk.yellow(\n `Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(tasksToMigrate.length / batchSize)}`\n )\n );\n\n for (const task of batch) {\n try {\n const newTask = await this.migrateTask(task, targetTeam.id);\n const mapping = {\n sourceId: task.id,\n sourceIdentifier: task.identifier,\n targetId: newTask.id,\n targetIdentifier: newTask.identifier,\n deleted: false,\n };\n\n result.imported++;\n console.log(\n chalk.green(\n `${task.identifier} \u2192 ${newTask.identifier}: ${task.title}`\n )\n );\n\n // Delete from source if configured\n if (this.config.deleteFromSource) {\n try {\n await this.deleteTask(task.id);\n mapping.deleted = true;\n result.deleted++;\n console.log(\n chalk.gray(`Deleted ${task.identifier} from source`)\n );\n } catch (deleteError: unknown) {\n result.deleteFailed++;\n result.errors.push(\n `Delete failed for ${task.identifier}: ${(deleteError as Error).message}`\n );\n console.log(\n chalk.yellow(\n `Failed to delete ${task.identifier} from source: ${(deleteError as Error).message}`\n )\n );\n }\n }\n\n result.taskMappings.push(mapping);\n } catch (error: unknown) {\n const errorMsg = (error as Error).message;\n result.errors.push(`${task.identifier}: ${errorMsg}`);\n result.taskMappings.push({\n sourceId: task.id,\n sourceIdentifier: task.identifier,\n error: errorMsg,\n });\n result.failed++;\n console.log(chalk.red(`${task.identifier}: ${errorMsg}`));\n }\n }\n\n // Delay between batches to avoid rate limits\n if (i + batchSize < tasksToMigrate.length) {\n console.log(chalk.gray(`Waiting ${delayMs}ms before next batch...`));\n await this.delay(delayMs);\n }\n }\n } catch (error: unknown) {\n result.errors.push(`Migration failed: ${(error as Error).message}`);\n logger.error('Migration failed:', error as Error);\n }\n\n return result;\n }\n\n /**\n * Migrate a single task\n */\n private async migrateTask(\n sourceTask: any,\n targetTeamId: string\n ): Promise<any> {\n // Map states from source to target format\n const stateMapping: Record<string, string> = {\n backlog: 'backlog',\n unstarted: 'unstarted',\n started: 'started',\n completed: 'completed',\n canceled: 'canceled',\n };\n\n // Create task in target workspace using GraphQL\n const createTaskQuery = `\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n // Prepare task input\n const taskInput = {\n title: `[MIGRATED] ${sourceTask.title}`,\n description: this.formatMigratedDescription(sourceTask),\n teamId: targetTeamId,\n priority: this.mapPriority(sourceTask.priority),\n };\n\n const response = await this.targetClient.makeRequest<{\n data: {\n issueCreate: {\n success: boolean;\n issue: any;\n };\n };\n }>(createTaskQuery, { input: taskInput });\n\n if (!response.data?.issueCreate?.success) {\n throw new IntegrationError(\n 'Failed to create task in target workspace',\n ErrorCode.LINEAR_SYNC_FAILED,\n { sourceTaskId: sourceTask.id, sourceIdentifier: sourceTask.identifier }\n );\n }\n\n return response.data.issueCreate.issue;\n }\n\n /**\n * Format description with migration context\n */\n private formatMigratedDescription(sourceTask: any): string {\n let description = sourceTask.description || '';\n\n description += `\\n\\n---\\n**Migration Info:**\\n`;\n description += `- Original ID: ${sourceTask.identifier}\\n`;\n description += `- Migrated: ${new Date().toISOString()}\\n`;\n description += `- Original State: ${sourceTask.state.name}\\n`;\n\n if (sourceTask.assignee) {\n description += `- Original Assignee: ${sourceTask.assignee.name}\\n`;\n }\n\n if (sourceTask.estimate) {\n description += `- Original Estimate: ${sourceTask.estimate} points\\n`;\n }\n\n return description;\n }\n\n /**\n * Map priority values\n */\n private mapPriority(priority?: number): number {\n // Linear priorities: 0=none, 1=urgent, 2=high, 3=medium, 4=low\n return priority || 0;\n }\n\n /**\n * Delete a task from the source workspace\n */\n private async deleteTask(taskId: string): Promise<void> {\n const deleteQuery = `\n mutation DeleteIssue($id: String!) {\n issueDelete(id: $id) {\n success\n }\n }\n `;\n\n const response = await (this.sourceClient as any).makeRequest(deleteQuery, {\n id: taskId,\n });\n\n if (!response.data?.issueDelete?.success) {\n throw new IntegrationError(\n 'Failed to delete task from source workspace',\n ErrorCode.LINEAR_SYNC_FAILED,\n { taskId }\n );\n }\n }\n\n /**\n * Delay helper\n */\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n/**\n * CLI function to run migration\n */\nexport async function runMigration(config: MigrationConfig): Promise<void> {\n const migrator = new LinearMigrator(config);\n\n console.log(chalk.blue('Testing connections...'));\n const connectionTest = await migrator.testConnections();\n\n if (!connectionTest.source.success) {\n console.error(\n chalk.red(`Source connection failed: ${connectionTest.source.error}`)\n );\n return;\n }\n\n if (!connectionTest.target.success) {\n console.error(\n chalk.red(`Target connection failed: ${connectionTest.target.error}`)\n );\n return;\n }\n\n console.log(chalk.green('Both connections successful'));\n console.log(\n chalk.cyan(\n `Source: ${connectionTest.source.info.user.name} @ ${connectionTest.source.info.team.name}`\n )\n );\n console.log(\n chalk.cyan(\n `Target: ${connectionTest.target.info.user.name} @ ${connectionTest.target.info.team.name}`\n )\n );\n\n const result = await migrator.migrate();\n\n console.log(chalk.blue('\\nMigration Summary:'));\n console.log(` Total tasks: ${result.totalTasks}`);\n console.log(` Exported: ${result.exported}`);\n console.log(chalk.green(` Imported: ${result.imported}`));\n console.log(chalk.red(` Failed: ${result.failed}`));\n if (config.deleteFromSource) {\n console.log(chalk.gray(` Deleted: ${result.deleted}`));\n if (result.deleteFailed > 0) {\n console.log(chalk.yellow(` Delete failed: ${result.deleteFailed}`));\n }\n }\n\n if (result.errors.length > 0) {\n console.log(chalk.red('\\nErrors:'));\n result.errors.forEach((error) => console.log(chalk.red(` - ${error}`)));\n }\n\n if (result.imported > 0) {\n console.log(\n chalk.green(\n `\\nMigration completed! ${result.imported} tasks migrated successfully.`\n )\n );\n if (config.deleteFromSource && result.deleted > 0) {\n console.log(\n chalk.gray(` ${result.deleted} tasks deleted from source workspace.`)\n );\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,11 +7,15 @@ import { URL } from "url";
|
|
|
7
7
|
import { logger } from "../../core/monitoring/logger.js";
|
|
8
8
|
import { LinearAuthManager } from "./auth.js";
|
|
9
9
|
import chalk from "chalk";
|
|
10
|
+
import { IntegrationError, ErrorCode } from "../../core/errors/index.js";
|
|
10
11
|
function getEnv(key, defaultValue) {
|
|
11
12
|
const value = process.env[key];
|
|
12
13
|
if (value === void 0) {
|
|
13
14
|
if (defaultValue !== void 0) return defaultValue;
|
|
14
|
-
throw new
|
|
15
|
+
throw new IntegrationError(
|
|
16
|
+
`Environment variable ${key} is required`,
|
|
17
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
18
|
+
);
|
|
15
19
|
}
|
|
16
20
|
return value;
|
|
17
21
|
}
|
|
@@ -49,10 +53,12 @@ class LinearOAuthServer {
|
|
|
49
53
|
const { code, state, error, error_description } = req.query;
|
|
50
54
|
if (error) {
|
|
51
55
|
logger.error(`OAuth error: ${error} - ${error_description}`);
|
|
52
|
-
res.send(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
res.send(
|
|
57
|
+
this.generateErrorPage(
|
|
58
|
+
"Authorization Failed",
|
|
59
|
+
`${error}: ${error_description || "An error occurred during authorization"}`
|
|
60
|
+
)
|
|
61
|
+
);
|
|
56
62
|
if (state && this.authCompleteCallbacks.has(state)) {
|
|
57
63
|
this.authCompleteCallbacks.get(state)(false);
|
|
58
64
|
this.authCompleteCallbacks.delete(state);
|
|
@@ -61,20 +67,28 @@ class LinearOAuthServer {
|
|
|
61
67
|
return;
|
|
62
68
|
}
|
|
63
69
|
if (!code) {
|
|
64
|
-
res.send(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
res.send(
|
|
71
|
+
this.generateErrorPage(
|
|
72
|
+
"Missing Authorization Code",
|
|
73
|
+
"No authorization code was provided in the callback"
|
|
74
|
+
)
|
|
75
|
+
);
|
|
68
76
|
this.scheduleShutdown();
|
|
69
77
|
return;
|
|
70
78
|
}
|
|
71
79
|
try {
|
|
72
80
|
const codeVerifier = state ? this.pendingCodeVerifiers.get(state) : process.env["_LINEAR_CODE_VERIFIER"];
|
|
73
81
|
if (!codeVerifier) {
|
|
74
|
-
throw new
|
|
82
|
+
throw new IntegrationError(
|
|
83
|
+
"Code verifier not found. Please restart the authorization process.",
|
|
84
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
85
|
+
);
|
|
75
86
|
}
|
|
76
87
|
logger.info("Exchanging authorization code for tokens...");
|
|
77
|
-
await this.authManager.exchangeCodeForToken(
|
|
88
|
+
await this.authManager.exchangeCodeForToken(
|
|
89
|
+
code,
|
|
90
|
+
codeVerifier
|
|
91
|
+
);
|
|
78
92
|
if (state) {
|
|
79
93
|
this.pendingCodeVerifiers.delete(state);
|
|
80
94
|
}
|
|
@@ -84,7 +98,10 @@ class LinearOAuthServer {
|
|
|
84
98
|
res.send(this.generateSuccessPage());
|
|
85
99
|
logger.info("Linear OAuth authentication completed successfully!");
|
|
86
100
|
} else {
|
|
87
|
-
throw new
|
|
101
|
+
throw new IntegrationError(
|
|
102
|
+
"Failed to verify Linear connection",
|
|
103
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
104
|
+
);
|
|
88
105
|
}
|
|
89
106
|
if (state && this.authCompleteCallbacks.has(state)) {
|
|
90
107
|
this.authCompleteCallbacks.get(state)(true);
|
|
@@ -93,10 +110,12 @@ class LinearOAuthServer {
|
|
|
93
110
|
this.scheduleShutdown();
|
|
94
111
|
} catch (error2) {
|
|
95
112
|
logger.error("Failed to complete OAuth flow:", error2);
|
|
96
|
-
res.send(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
res.send(
|
|
114
|
+
this.generateErrorPage(
|
|
115
|
+
"Authentication Failed",
|
|
116
|
+
error2.message
|
|
117
|
+
)
|
|
118
|
+
);
|
|
100
119
|
if (state && this.authCompleteCallbacks.has(state)) {
|
|
101
120
|
this.authCompleteCallbacks.get(state)(false);
|
|
102
121
|
this.authCompleteCallbacks.delete(state);
|
|
@@ -108,10 +127,12 @@ class LinearOAuthServer {
|
|
|
108
127
|
try {
|
|
109
128
|
const config = this.authManager.loadConfig();
|
|
110
129
|
if (!config) {
|
|
111
|
-
res.status(400).send(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
res.status(400).send(
|
|
131
|
+
this.generateErrorPage(
|
|
132
|
+
"Configuration Missing",
|
|
133
|
+
"Linear OAuth configuration not found. Please configure your client ID and secret."
|
|
134
|
+
)
|
|
135
|
+
);
|
|
115
136
|
return;
|
|
116
137
|
}
|
|
117
138
|
const state = this.generateState();
|
|
@@ -120,10 +141,12 @@ class LinearOAuthServer {
|
|
|
120
141
|
res.redirect(url);
|
|
121
142
|
} catch (error) {
|
|
122
143
|
logger.error("Failed to start OAuth flow:", error);
|
|
123
|
-
res.status(500).send(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
res.status(500).send(
|
|
145
|
+
this.generateErrorPage(
|
|
146
|
+
"OAuth Start Failed",
|
|
147
|
+
error.message
|
|
148
|
+
)
|
|
149
|
+
);
|
|
127
150
|
}
|
|
128
151
|
});
|
|
129
152
|
this.app.use((req, res) => {
|
|
@@ -277,7 +300,7 @@ class LinearOAuthServer {
|
|
|
277
300
|
const response = await fetch("https://api.linear.app/graphql", {
|
|
278
301
|
method: "POST",
|
|
279
302
|
headers: {
|
|
280
|
-
|
|
303
|
+
Authorization: `Bearer ${token}`,
|
|
281
304
|
"Content-Type": "application/json"
|
|
282
305
|
},
|
|
283
306
|
body: JSON.stringify({
|
|
@@ -287,7 +310,9 @@ class LinearOAuthServer {
|
|
|
287
310
|
if (response.ok) {
|
|
288
311
|
const result = await response.json();
|
|
289
312
|
if (result.data?.viewer) {
|
|
290
|
-
logger.info(
|
|
313
|
+
logger.info(
|
|
314
|
+
`Connected to Linear as: ${result.data.viewer.name} (${result.data.viewer.email})`
|
|
315
|
+
);
|
|
291
316
|
return true;
|
|
292
317
|
}
|
|
293
318
|
}
|
|
@@ -315,16 +340,26 @@ class LinearOAuthServer {
|
|
|
315
340
|
this.config.port,
|
|
316
341
|
this.config.host,
|
|
317
342
|
() => {
|
|
318
|
-
console.log(
|
|
343
|
+
console.log(
|
|
344
|
+
chalk.green("\u2713") + chalk.bold(" Linear OAuth Server Started")
|
|
345
|
+
);
|
|
319
346
|
console.log(chalk.cyan(" Authorization URL: ") + setupUrl);
|
|
320
|
-
console.log(
|
|
347
|
+
console.log(
|
|
348
|
+
chalk.cyan(" Callback URL: ") + `http://${this.config.host}:${this.config.port}${this.config.redirectPath}`
|
|
349
|
+
);
|
|
321
350
|
console.log("");
|
|
322
351
|
console.log(chalk.yellow(" \u26A0 Configuration Required:"));
|
|
323
|
-
console.log(
|
|
324
|
-
|
|
352
|
+
console.log(
|
|
353
|
+
" 1. Create a Linear OAuth app at: https://linear.app/settings/api"
|
|
354
|
+
);
|
|
355
|
+
console.log(
|
|
356
|
+
` 2. Set redirect URI to: http://${this.config.host}:${this.config.port}${this.config.redirectPath}`
|
|
357
|
+
);
|
|
325
358
|
console.log(" 3. Set environment variables:");
|
|
326
359
|
console.log(' export LINEAR_CLIENT_ID="your_client_id"');
|
|
327
|
-
console.log(
|
|
360
|
+
console.log(
|
|
361
|
+
' export LINEAR_CLIENT_SECRET="your_client_secret"'
|
|
362
|
+
);
|
|
328
363
|
console.log(" 4. Restart the auth process");
|
|
329
364
|
resolve({ url: setupUrl });
|
|
330
365
|
}
|
|
@@ -338,17 +373,25 @@ class LinearOAuthServer {
|
|
|
338
373
|
this.config.port,
|
|
339
374
|
this.config.host,
|
|
340
375
|
() => {
|
|
341
|
-
console.log(
|
|
376
|
+
console.log(
|
|
377
|
+
chalk.green("\u2713") + chalk.bold(" Linear OAuth Server Started")
|
|
378
|
+
);
|
|
342
379
|
console.log(chalk.cyan(" Open this URL in your browser:"));
|
|
343
380
|
console.log(" " + chalk.underline(url));
|
|
344
381
|
console.log("");
|
|
345
|
-
console.log(
|
|
382
|
+
console.log(
|
|
383
|
+
chalk.gray(
|
|
384
|
+
" The server will automatically shut down after authorization completes."
|
|
385
|
+
)
|
|
386
|
+
);
|
|
346
387
|
resolve({ url, codeVerifier });
|
|
347
388
|
}
|
|
348
389
|
);
|
|
349
390
|
this.authCompleteCallbacks.set(state, (success) => {
|
|
350
391
|
if (success) {
|
|
351
|
-
console.log(
|
|
392
|
+
console.log(
|
|
393
|
+
chalk.green("\n\u2713 Linear authorization completed successfully!")
|
|
394
|
+
);
|
|
352
395
|
} else {
|
|
353
396
|
console.log(chalk.red("\n\u2717 Linear authorization failed"));
|
|
354
397
|
}
|