@proletariat/cli 0.3.45 → 0.3.46
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/commands/config/index.js +39 -1
- package/dist/commands/linear/auth.d.ts +14 -0
- package/dist/commands/linear/auth.js +211 -0
- package/dist/commands/linear/import.d.ts +21 -0
- package/dist/commands/linear/import.js +260 -0
- package/dist/commands/linear/status.d.ts +11 -0
- package/dist/commands/linear/status.js +88 -0
- package/dist/commands/linear/sync.d.ts +15 -0
- package/dist/commands/linear/sync.js +233 -0
- package/dist/commands/orchestrator/attach.d.ts +9 -1
- package/dist/commands/orchestrator/attach.js +67 -13
- package/dist/commands/orchestrator/index.js +22 -7
- package/dist/commands/ticket/link/duplicates.d.ts +15 -0
- package/dist/commands/ticket/link/duplicates.js +95 -0
- package/dist/commands/ticket/link/index.js +14 -0
- package/dist/commands/ticket/link/relates.d.ts +15 -0
- package/dist/commands/ticket/link/relates.js +95 -0
- package/dist/commands/work/revise.js +4 -3
- package/dist/commands/work/spawn.d.ts +5 -0
- package/dist/commands/work/spawn.js +195 -14
- package/dist/commands/work/start.js +75 -19
- package/dist/lib/execution/config.d.ts +15 -0
- package/dist/lib/execution/config.js +54 -0
- package/dist/lib/execution/devcontainer.d.ts +6 -3
- package/dist/lib/execution/devcontainer.js +39 -12
- package/dist/lib/execution/runners.d.ts +28 -32
- package/dist/lib/execution/runners.js +345 -275
- package/dist/lib/execution/spawner.js +62 -5
- package/dist/lib/execution/types.d.ts +4 -0
- package/dist/lib/execution/types.js +3 -0
- package/dist/lib/external-issues/adapters.d.ts +26 -0
- package/dist/lib/external-issues/adapters.js +251 -0
- package/dist/lib/external-issues/index.d.ts +10 -0
- package/dist/lib/external-issues/index.js +14 -0
- package/dist/lib/external-issues/mapper.d.ts +21 -0
- package/dist/lib/external-issues/mapper.js +86 -0
- package/dist/lib/external-issues/types.d.ts +144 -0
- package/dist/lib/external-issues/types.js +26 -0
- package/dist/lib/external-issues/validation.d.ts +34 -0
- package/dist/lib/external-issues/validation.js +219 -0
- package/dist/lib/linear/client.d.ts +55 -0
- package/dist/lib/linear/client.js +254 -0
- package/dist/lib/linear/config.d.ts +37 -0
- package/dist/lib/linear/config.js +100 -0
- package/dist/lib/linear/index.d.ts +11 -0
- package/dist/lib/linear/index.js +10 -0
- package/dist/lib/linear/mapper.d.ts +67 -0
- package/dist/lib/linear/mapper.js +219 -0
- package/dist/lib/linear/sync.d.ts +37 -0
- package/dist/lib/linear/sync.js +89 -0
- package/dist/lib/linear/types.d.ts +139 -0
- package/dist/lib/linear/types.js +34 -0
- package/dist/lib/mcp/helpers.d.ts +8 -0
- package/dist/lib/mcp/helpers.js +10 -0
- package/dist/lib/mcp/tools/board.js +63 -11
- package/dist/lib/pmo/schema.d.ts +2 -0
- package/dist/lib/pmo/schema.js +20 -0
- package/dist/lib/pmo/storage/base.js +92 -13
- package/dist/lib/pmo/storage/dependencies.js +15 -0
- package/dist/lib/prompt-json.d.ts +4 -0
- package/oclif.manifest.json +2867 -2380
- package/package.json +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Configuration Storage
|
|
3
|
+
*
|
|
4
|
+
* Stores Linear credentials and preferences in the workspace_settings table.
|
|
5
|
+
*/
|
|
6
|
+
const SETTINGS_TABLE = 'workspace_settings';
|
|
7
|
+
// Config keys stored in workspace_settings table
|
|
8
|
+
const LINEAR_CONFIG_KEYS = {
|
|
9
|
+
apiKey: 'linear.api_key',
|
|
10
|
+
defaultTeamId: 'linear.default_team_id',
|
|
11
|
+
defaultTeamKey: 'linear.default_team_key',
|
|
12
|
+
organizationName: 'linear.organization_name',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Get a setting value from the database.
|
|
16
|
+
*/
|
|
17
|
+
function getSetting(db, key) {
|
|
18
|
+
const row = db
|
|
19
|
+
.prepare(`SELECT value FROM ${SETTINGS_TABLE} WHERE key = ?`)
|
|
20
|
+
.get(key);
|
|
21
|
+
return row?.value ?? null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Set a setting value in the database.
|
|
25
|
+
*/
|
|
26
|
+
function setSetting(db, key, value) {
|
|
27
|
+
db.prepare(`
|
|
28
|
+
INSERT INTO ${SETTINGS_TABLE} (key, value)
|
|
29
|
+
VALUES (?, ?)
|
|
30
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
31
|
+
`).run(key, value);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Delete a setting from the database.
|
|
35
|
+
*/
|
|
36
|
+
function deleteSetting(db, key) {
|
|
37
|
+
db.prepare(`DELETE FROM ${SETTINGS_TABLE} WHERE key = ?`).run(key);
|
|
38
|
+
}
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Public API
|
|
41
|
+
// =============================================================================
|
|
42
|
+
/**
|
|
43
|
+
* Check if Linear is configured (API key is stored).
|
|
44
|
+
*/
|
|
45
|
+
export function isLinearConfigured(db) {
|
|
46
|
+
return getSetting(db, LINEAR_CONFIG_KEYS.apiKey) !== null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Load Linear configuration from the database.
|
|
50
|
+
* Returns null if not configured.
|
|
51
|
+
*/
|
|
52
|
+
export function loadLinearConfig(db) {
|
|
53
|
+
const apiKey = getSetting(db, LINEAR_CONFIG_KEYS.apiKey);
|
|
54
|
+
if (!apiKey)
|
|
55
|
+
return null;
|
|
56
|
+
return {
|
|
57
|
+
apiKey,
|
|
58
|
+
defaultTeamId: getSetting(db, LINEAR_CONFIG_KEYS.defaultTeamId) ?? undefined,
|
|
59
|
+
defaultTeamKey: getSetting(db, LINEAR_CONFIG_KEYS.defaultTeamKey) ?? undefined,
|
|
60
|
+
organizationName: getSetting(db, LINEAR_CONFIG_KEYS.organizationName) ?? undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Save Linear API key to the database.
|
|
65
|
+
*/
|
|
66
|
+
export function saveLinearApiKey(db, apiKey) {
|
|
67
|
+
setSetting(db, LINEAR_CONFIG_KEYS.apiKey, apiKey);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Save the default team for Linear operations.
|
|
71
|
+
*/
|
|
72
|
+
export function saveLinearDefaultTeam(db, teamId, teamKey) {
|
|
73
|
+
setSetting(db, LINEAR_CONFIG_KEYS.defaultTeamId, teamId);
|
|
74
|
+
setSetting(db, LINEAR_CONFIG_KEYS.defaultTeamKey, teamKey);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Save the organization name.
|
|
78
|
+
*/
|
|
79
|
+
export function saveLinearOrganization(db, name) {
|
|
80
|
+
setSetting(db, LINEAR_CONFIG_KEYS.organizationName, name);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Clear all Linear configuration from the database.
|
|
84
|
+
*/
|
|
85
|
+
export function clearLinearConfig(db) {
|
|
86
|
+
for (const key of Object.values(LINEAR_CONFIG_KEYS)) {
|
|
87
|
+
deleteSetting(db, key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get the stored Linear API key.
|
|
92
|
+
* Also checks PRLT_LINEAR_API_KEY and LINEAR_API_KEY environment variables.
|
|
93
|
+
*/
|
|
94
|
+
export function getLinearApiKey(db) {
|
|
95
|
+
// Environment variables take precedence
|
|
96
|
+
const envKey = process.env.PRLT_LINEAR_API_KEY || process.env.LINEAR_API_KEY;
|
|
97
|
+
if (envKey)
|
|
98
|
+
return envKey;
|
|
99
|
+
return getSetting(db, LINEAR_CONFIG_KEYS.apiKey);
|
|
100
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Integration Module
|
|
3
|
+
*
|
|
4
|
+
* Provides Linear API access, PMO mapping, and sync capabilities.
|
|
5
|
+
*/
|
|
6
|
+
export { LinearClient } from './client.js';
|
|
7
|
+
export { isLinearConfigured, loadLinearConfig, saveLinearApiKey, saveLinearDefaultTeam, saveLinearOrganization, clearLinearConfig, getLinearApiKey, } from './config.js';
|
|
8
|
+
export { LinearMapper } from './mapper.js';
|
|
9
|
+
export { LinearSync } from './sync.js';
|
|
10
|
+
export type { LinearIssue, LinearTeam, LinearWorkflowState, LinearCycle, LinearConfig, LinearIssueFilter, LinearIssueMap, LinearSyncResult, } from './types.js';
|
|
11
|
+
export { LINEAR_STATE_TO_PMO_CATEGORY, LINEAR_PRIORITY_TO_PMO, PMO_PRIORITY_TO_LINEAR, } from './types.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Integration Module
|
|
3
|
+
*
|
|
4
|
+
* Provides Linear API access, PMO mapping, and sync capabilities.
|
|
5
|
+
*/
|
|
6
|
+
export { LinearClient } from './client.js';
|
|
7
|
+
export { isLinearConfigured, loadLinearConfig, saveLinearApiKey, saveLinearDefaultTeam, saveLinearOrganization, clearLinearConfig, getLinearApiKey, } from './config.js';
|
|
8
|
+
export { LinearMapper } from './mapper.js';
|
|
9
|
+
export { LinearSync } from './sync.js';
|
|
10
|
+
export { LINEAR_STATE_TO_PMO_CATEGORY, LINEAR_PRIORITY_TO_PMO, PMO_PRIORITY_TO_LINEAR, } from './types.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Issue ↔ PMO Ticket Mapper
|
|
3
|
+
*
|
|
4
|
+
* Converts Linear issues to PMO tickets and manages the mapping table.
|
|
5
|
+
* Handles import (Linear → PMO) and reverse lookup for sync operations.
|
|
6
|
+
*/
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import type { CreateTicketInput, PMOStorage, WorkflowStatus } from '../pmo/types.js';
|
|
9
|
+
import type { LinearIssue, LinearIssueMap, LinearSyncResult } from './types.js';
|
|
10
|
+
export declare class LinearMapper {
|
|
11
|
+
private db;
|
|
12
|
+
constructor(db: Database.Database);
|
|
13
|
+
/**
|
|
14
|
+
* Ensure the linear_issue_map table exists.
|
|
15
|
+
* Uses CREATE TABLE IF NOT EXISTS to match the schema defined in schema.ts.
|
|
16
|
+
*/
|
|
17
|
+
private ensureTable;
|
|
18
|
+
/**
|
|
19
|
+
* Convert a Linear issue to a PMO ticket creation input.
|
|
20
|
+
* Finds the matching workflow status in the project's workflow.
|
|
21
|
+
*/
|
|
22
|
+
issueToTicketInput(issue: LinearIssue, statuses: WorkflowStatus[]): CreateTicketInput;
|
|
23
|
+
/**
|
|
24
|
+
* Import a single Linear issue into PMO as a ticket.
|
|
25
|
+
* Returns the PMO ticket ID if created, null if already mapped.
|
|
26
|
+
*/
|
|
27
|
+
importIssue(issue: LinearIssue, projectId: string, storage: PMOStorage, statuses: WorkflowStatus[]): Promise<{
|
|
28
|
+
ticketId: string;
|
|
29
|
+
created: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Batch import multiple Linear issues into PMO.
|
|
33
|
+
*/
|
|
34
|
+
importIssues(issues: LinearIssue[], projectId: string, storage: PMOStorage, statuses: WorkflowStatus[]): Promise<LinearSyncResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Create a mapping record.
|
|
37
|
+
*/
|
|
38
|
+
createMapping(map: Omit<LinearIssueMap, 'lastSyncedAt'>): void;
|
|
39
|
+
/**
|
|
40
|
+
* Get a mapping by PMO ticket ID.
|
|
41
|
+
*/
|
|
42
|
+
getByTicketId(ticketId: string): LinearIssueMap | null;
|
|
43
|
+
/**
|
|
44
|
+
* Get a mapping by Linear issue ID.
|
|
45
|
+
*/
|
|
46
|
+
getByLinearId(linearIssueId: string): LinearIssueMap | null;
|
|
47
|
+
/**
|
|
48
|
+
* Get a mapping by Linear identifier (e.g., "ENG-123").
|
|
49
|
+
*/
|
|
50
|
+
getByIdentifier(identifier: string): LinearIssueMap | null;
|
|
51
|
+
/**
|
|
52
|
+
* List all mappings.
|
|
53
|
+
*/
|
|
54
|
+
listMappings(): LinearIssueMap[];
|
|
55
|
+
/**
|
|
56
|
+
* Update the last synced timestamp for a mapping.
|
|
57
|
+
*/
|
|
58
|
+
updateSyncTimestamp(pmoTicketId: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Delete a mapping by PMO ticket ID.
|
|
61
|
+
*/
|
|
62
|
+
deleteMapping(pmoTicketId: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* Convert a database row to a LinearIssueMap.
|
|
65
|
+
*/
|
|
66
|
+
private rowToMap;
|
|
67
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Issue ↔ PMO Ticket Mapper
|
|
3
|
+
*
|
|
4
|
+
* Converts Linear issues to PMO tickets and manages the mapping table.
|
|
5
|
+
* Handles import (Linear → PMO) and reverse lookup for sync operations.
|
|
6
|
+
*/
|
|
7
|
+
import { PMO_TABLES } from '../pmo/schema.js';
|
|
8
|
+
import { LINEAR_STATE_TO_PMO_CATEGORY, LINEAR_PRIORITY_TO_PMO, } from './types.js';
|
|
9
|
+
export class LinearMapper {
|
|
10
|
+
db;
|
|
11
|
+
constructor(db) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
this.ensureTable();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Ensure the linear_issue_map table exists.
|
|
17
|
+
* Uses CREATE TABLE IF NOT EXISTS to match the schema defined in schema.ts.
|
|
18
|
+
*/
|
|
19
|
+
ensureTable() {
|
|
20
|
+
this.db.exec(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS ${PMO_TABLES.linear_issue_map} (
|
|
22
|
+
pmo_ticket_id TEXT NOT NULL REFERENCES ${PMO_TABLES.tickets}(id) ON DELETE CASCADE,
|
|
23
|
+
linear_issue_id TEXT NOT NULL,
|
|
24
|
+
linear_identifier TEXT NOT NULL,
|
|
25
|
+
linear_team_key TEXT NOT NULL,
|
|
26
|
+
linear_url TEXT NOT NULL,
|
|
27
|
+
sync_direction TEXT NOT NULL DEFAULT 'inbound',
|
|
28
|
+
last_synced_at TIMESTAMP,
|
|
29
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
PRIMARY KEY (pmo_ticket_id),
|
|
31
|
+
UNIQUE (linear_issue_id)
|
|
32
|
+
)
|
|
33
|
+
`);
|
|
34
|
+
this.db.exec(`
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_pmo_linear_issue_map_linear_id
|
|
36
|
+
ON ${PMO_TABLES.linear_issue_map}(linear_issue_id)
|
|
37
|
+
`);
|
|
38
|
+
this.db.exec(`
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_pmo_linear_issue_map_identifier
|
|
40
|
+
ON ${PMO_TABLES.linear_issue_map}(linear_identifier)
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert a Linear issue to a PMO ticket creation input.
|
|
45
|
+
* Finds the matching workflow status in the project's workflow.
|
|
46
|
+
*/
|
|
47
|
+
issueToTicketInput(issue, statuses) {
|
|
48
|
+
// Map Linear state type to PMO state category
|
|
49
|
+
const pmoCategory = LINEAR_STATE_TO_PMO_CATEGORY[issue.state.type] ?? 'backlog';
|
|
50
|
+
// Find matching status by category
|
|
51
|
+
const matchingStatus = statuses.find((s) => s.category === pmoCategory);
|
|
52
|
+
const fallbackStatus = statuses.find((s) => s.isDefault) ?? statuses[0];
|
|
53
|
+
const targetStatus = matchingStatus ?? fallbackStatus;
|
|
54
|
+
// Map priority
|
|
55
|
+
const pmoPriority = LINEAR_PRIORITY_TO_PMO[issue.priority] ?? 'P2';
|
|
56
|
+
// Build description with Linear reference
|
|
57
|
+
const descriptionParts = [];
|
|
58
|
+
if (issue.description) {
|
|
59
|
+
descriptionParts.push(issue.description);
|
|
60
|
+
}
|
|
61
|
+
descriptionParts.push('');
|
|
62
|
+
descriptionParts.push(`---`);
|
|
63
|
+
descriptionParts.push(`_Imported from Linear: [${issue.identifier}](${issue.url})_`);
|
|
64
|
+
// Map labels
|
|
65
|
+
const labels = issue.labels.map((l) => l.name);
|
|
66
|
+
return {
|
|
67
|
+
title: issue.title,
|
|
68
|
+
description: descriptionParts.join('\n'),
|
|
69
|
+
priority: pmoPriority,
|
|
70
|
+
statusId: targetStatus?.id,
|
|
71
|
+
labels,
|
|
72
|
+
metadata: {
|
|
73
|
+
'linear.issue_id': issue.id,
|
|
74
|
+
'linear.identifier': issue.identifier,
|
|
75
|
+
'linear.url': issue.url,
|
|
76
|
+
'linear.team': issue.team.key,
|
|
77
|
+
'linear.state': issue.state.name,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Import a single Linear issue into PMO as a ticket.
|
|
83
|
+
* Returns the PMO ticket ID if created, null if already mapped.
|
|
84
|
+
*/
|
|
85
|
+
async importIssue(issue, projectId, storage, statuses) {
|
|
86
|
+
// Check if already mapped
|
|
87
|
+
const existing = this.getByLinearId(issue.id);
|
|
88
|
+
if (existing) {
|
|
89
|
+
return { ticketId: existing.pmoTicketId, created: false };
|
|
90
|
+
}
|
|
91
|
+
// Convert to ticket input
|
|
92
|
+
const ticketInput = this.issueToTicketInput(issue, statuses);
|
|
93
|
+
// Create the PMO ticket
|
|
94
|
+
const ticket = await storage.createTicket(projectId, ticketInput);
|
|
95
|
+
// Record the mapping
|
|
96
|
+
this.createMapping({
|
|
97
|
+
pmoTicketId: ticket.id,
|
|
98
|
+
linearIssueId: issue.id,
|
|
99
|
+
linearIdentifier: issue.identifier,
|
|
100
|
+
linearTeamKey: issue.team.key,
|
|
101
|
+
linearUrl: issue.url,
|
|
102
|
+
syncDirection: 'inbound',
|
|
103
|
+
createdAt: new Date(),
|
|
104
|
+
});
|
|
105
|
+
return { ticketId: ticket.id, created: true };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Batch import multiple Linear issues into PMO.
|
|
109
|
+
*/
|
|
110
|
+
async importIssues(issues, projectId, storage, statuses) {
|
|
111
|
+
const result = {
|
|
112
|
+
imported: 0,
|
|
113
|
+
updated: 0,
|
|
114
|
+
skipped: 0,
|
|
115
|
+
errors: [],
|
|
116
|
+
};
|
|
117
|
+
for (const issue of issues) {
|
|
118
|
+
try {
|
|
119
|
+
// eslint-disable-next-line no-await-in-loop
|
|
120
|
+
const { created } = await this.importIssue(issue, projectId, storage, statuses);
|
|
121
|
+
if (created) {
|
|
122
|
+
result.imported++;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
result.skipped++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
result.errors.push({
|
|
130
|
+
identifier: issue.identifier,
|
|
131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
// ===========================================================================
|
|
138
|
+
// Mapping CRUD
|
|
139
|
+
// ===========================================================================
|
|
140
|
+
/**
|
|
141
|
+
* Create a mapping record.
|
|
142
|
+
*/
|
|
143
|
+
createMapping(map) {
|
|
144
|
+
this.db.prepare(`
|
|
145
|
+
INSERT INTO ${PMO_TABLES.linear_issue_map}
|
|
146
|
+
(pmo_ticket_id, linear_issue_id, linear_identifier, linear_team_key, linear_url, sync_direction, last_synced_at, created_at)
|
|
147
|
+
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
148
|
+
`).run(map.pmoTicketId, map.linearIssueId, map.linearIdentifier, map.linearTeamKey, map.linearUrl, map.syncDirection);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get a mapping by PMO ticket ID.
|
|
152
|
+
*/
|
|
153
|
+
getByTicketId(ticketId) {
|
|
154
|
+
const row = this.db.prepare(`
|
|
155
|
+
SELECT * FROM ${PMO_TABLES.linear_issue_map} WHERE pmo_ticket_id = ?
|
|
156
|
+
`).get(ticketId);
|
|
157
|
+
return row ? this.rowToMap(row) : null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get a mapping by Linear issue ID.
|
|
161
|
+
*/
|
|
162
|
+
getByLinearId(linearIssueId) {
|
|
163
|
+
const row = this.db.prepare(`
|
|
164
|
+
SELECT * FROM ${PMO_TABLES.linear_issue_map} WHERE linear_issue_id = ?
|
|
165
|
+
`).get(linearIssueId);
|
|
166
|
+
return row ? this.rowToMap(row) : null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get a mapping by Linear identifier (e.g., "ENG-123").
|
|
170
|
+
*/
|
|
171
|
+
getByIdentifier(identifier) {
|
|
172
|
+
const row = this.db.prepare(`
|
|
173
|
+
SELECT * FROM ${PMO_TABLES.linear_issue_map} WHERE linear_identifier = ?
|
|
174
|
+
`).get(identifier);
|
|
175
|
+
return row ? this.rowToMap(row) : null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* List all mappings.
|
|
179
|
+
*/
|
|
180
|
+
listMappings() {
|
|
181
|
+
const rows = this.db.prepare(`
|
|
182
|
+
SELECT * FROM ${PMO_TABLES.linear_issue_map} ORDER BY created_at DESC
|
|
183
|
+
`).all();
|
|
184
|
+
return rows.map((row) => this.rowToMap(row));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Update the last synced timestamp for a mapping.
|
|
188
|
+
*/
|
|
189
|
+
updateSyncTimestamp(pmoTicketId) {
|
|
190
|
+
this.db.prepare(`
|
|
191
|
+
UPDATE ${PMO_TABLES.linear_issue_map}
|
|
192
|
+
SET last_synced_at = CURRENT_TIMESTAMP
|
|
193
|
+
WHERE pmo_ticket_id = ?
|
|
194
|
+
`).run(pmoTicketId);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Delete a mapping by PMO ticket ID.
|
|
198
|
+
*/
|
|
199
|
+
deleteMapping(pmoTicketId) {
|
|
200
|
+
this.db.prepare(`
|
|
201
|
+
DELETE FROM ${PMO_TABLES.linear_issue_map} WHERE pmo_ticket_id = ?
|
|
202
|
+
`).run(pmoTicketId);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Convert a database row to a LinearIssueMap.
|
|
206
|
+
*/
|
|
207
|
+
rowToMap(row) {
|
|
208
|
+
return {
|
|
209
|
+
pmoTicketId: row.pmo_ticket_id,
|
|
210
|
+
linearIssueId: row.linear_issue_id,
|
|
211
|
+
linearIdentifier: row.linear_identifier,
|
|
212
|
+
linearTeamKey: row.linear_team_key,
|
|
213
|
+
linearUrl: row.linear_url,
|
|
214
|
+
syncDirection: row.sync_direction,
|
|
215
|
+
lastSyncedAt: row.last_synced_at ? new Date(row.last_synced_at) : undefined,
|
|
216
|
+
createdAt: new Date(row.created_at),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Outbound Sync
|
|
3
|
+
*
|
|
4
|
+
* Handles syncing PMO ticket status changes back to Linear,
|
|
5
|
+
* attaching PR links, and posting status comments.
|
|
6
|
+
*/
|
|
7
|
+
import type { PMOStorage, Ticket } from '../pmo/types.js';
|
|
8
|
+
import { LinearClient } from './client.js';
|
|
9
|
+
import { LinearMapper } from './mapper.js';
|
|
10
|
+
import type { LinearWorkflowState } from './types.js';
|
|
11
|
+
export declare class LinearSync {
|
|
12
|
+
private client;
|
|
13
|
+
private mapper;
|
|
14
|
+
constructor(client: LinearClient, mapper: LinearMapper);
|
|
15
|
+
/**
|
|
16
|
+
* Sync a PMO ticket's status back to Linear.
|
|
17
|
+
* Finds the best matching Linear state based on category.
|
|
18
|
+
*/
|
|
19
|
+
syncTicketStatus(ticket: Ticket, linearStates: LinearWorkflowState[]): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Attach a PR link to the corresponding Linear issue.
|
|
22
|
+
*/
|
|
23
|
+
syncPRLink(ticketId: string, prUrl: string, prTitle: string): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Post a status update comment to the Linear issue.
|
|
26
|
+
*/
|
|
27
|
+
syncStatusComment(ticketId: string, statusName: string, message?: string): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Batch sync all mapped tickets that have changed since last sync.
|
|
30
|
+
* Returns counts of synced/skipped/errored items.
|
|
31
|
+
*/
|
|
32
|
+
syncAllStatuses(storage: PMOStorage, mappings: ReturnType<LinearMapper['listMappings']>, linearStates: LinearWorkflowState[]): Promise<{
|
|
33
|
+
synced: number;
|
|
34
|
+
skipped: number;
|
|
35
|
+
errors: number;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Outbound Sync
|
|
3
|
+
*
|
|
4
|
+
* Handles syncing PMO ticket status changes back to Linear,
|
|
5
|
+
* attaching PR links, and posting status comments.
|
|
6
|
+
*/
|
|
7
|
+
export class LinearSync {
|
|
8
|
+
client;
|
|
9
|
+
mapper;
|
|
10
|
+
constructor(client, mapper) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
this.mapper = mapper;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sync a PMO ticket's status back to Linear.
|
|
16
|
+
* Finds the best matching Linear state based on category.
|
|
17
|
+
*/
|
|
18
|
+
async syncTicketStatus(ticket, linearStates) {
|
|
19
|
+
const mapping = this.mapper.getByTicketId(ticket.id);
|
|
20
|
+
if (!mapping)
|
|
21
|
+
return false;
|
|
22
|
+
const pmoCategory = ticket.statusCategory;
|
|
23
|
+
if (!pmoCategory)
|
|
24
|
+
return false;
|
|
25
|
+
// Find the best matching Linear state
|
|
26
|
+
const matchingState = linearStates.find((s) => s.type === pmoCategory);
|
|
27
|
+
if (!matchingState)
|
|
28
|
+
return false;
|
|
29
|
+
await this.client.updateIssueState(mapping.linearIssueId, matchingState.id);
|
|
30
|
+
this.mapper.updateSyncTimestamp(ticket.id);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Attach a PR link to the corresponding Linear issue.
|
|
35
|
+
*/
|
|
36
|
+
async syncPRLink(ticketId, prUrl, prTitle) {
|
|
37
|
+
const mapping = this.mapper.getByTicketId(ticketId);
|
|
38
|
+
if (!mapping)
|
|
39
|
+
return false;
|
|
40
|
+
await this.client.attachUrl(mapping.linearIssueId, prUrl, `PR: ${prTitle}`);
|
|
41
|
+
// Also add a comment for visibility
|
|
42
|
+
await this.client.addComment(mapping.linearIssueId, `Pull request created: [${prTitle}](${prUrl})`);
|
|
43
|
+
this.mapper.updateSyncTimestamp(ticketId);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Post a status update comment to the Linear issue.
|
|
48
|
+
*/
|
|
49
|
+
async syncStatusComment(ticketId, statusName, message) {
|
|
50
|
+
const mapping = this.mapper.getByTicketId(ticketId);
|
|
51
|
+
if (!mapping)
|
|
52
|
+
return false;
|
|
53
|
+
const body = message
|
|
54
|
+
? `Status updated to **${statusName}**: ${message}`
|
|
55
|
+
: `Status updated to **${statusName}**`;
|
|
56
|
+
await this.client.addComment(mapping.linearIssueId, body);
|
|
57
|
+
this.mapper.updateSyncTimestamp(ticketId);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Batch sync all mapped tickets that have changed since last sync.
|
|
62
|
+
* Returns counts of synced/skipped/errored items.
|
|
63
|
+
*/
|
|
64
|
+
async syncAllStatuses(storage, mappings, linearStates) {
|
|
65
|
+
const result = { synced: 0, skipped: 0, errors: 0 };
|
|
66
|
+
for (const mapping of mappings) {
|
|
67
|
+
try {
|
|
68
|
+
// eslint-disable-next-line no-await-in-loop
|
|
69
|
+
const ticket = await storage.getTicket(mapping.pmoTicketId);
|
|
70
|
+
if (!ticket) {
|
|
71
|
+
result.skipped++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// eslint-disable-next-line no-await-in-loop
|
|
75
|
+
const synced = await this.syncTicketStatus(ticket, linearStates);
|
|
76
|
+
if (synced) {
|
|
77
|
+
result.synced++;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
result.skipped++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
result.errors++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Integration Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the Linear ↔ PMO integration layer.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Represents a Linear issue as fetched from the API.
|
|
8
|
+
*/
|
|
9
|
+
export interface LinearIssue {
|
|
10
|
+
id: string;
|
|
11
|
+
identifier: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
priority: number;
|
|
15
|
+
state: {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
type: string;
|
|
19
|
+
};
|
|
20
|
+
team: {
|
|
21
|
+
id: string;
|
|
22
|
+
key: string;
|
|
23
|
+
name: string;
|
|
24
|
+
};
|
|
25
|
+
assignee?: {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
email: string;
|
|
29
|
+
};
|
|
30
|
+
labels: Array<{
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
color: string;
|
|
34
|
+
}>;
|
|
35
|
+
cycle?: {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
number: number;
|
|
39
|
+
};
|
|
40
|
+
project?: {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
};
|
|
44
|
+
estimate?: number;
|
|
45
|
+
url: string;
|
|
46
|
+
createdAt: string;
|
|
47
|
+
updatedAt: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Represents a Linear team (workspace organizational unit).
|
|
51
|
+
*/
|
|
52
|
+
export interface LinearTeam {
|
|
53
|
+
id: string;
|
|
54
|
+
key: string;
|
|
55
|
+
name: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Represents a Linear workflow state.
|
|
60
|
+
*/
|
|
61
|
+
export interface LinearWorkflowState {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
type: string;
|
|
65
|
+
color: string;
|
|
66
|
+
position: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Represents a Linear cycle (sprint).
|
|
70
|
+
*/
|
|
71
|
+
export interface LinearCycle {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
number: number;
|
|
75
|
+
startsAt: string;
|
|
76
|
+
endsAt: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Linear connection configuration stored in workspace_settings.
|
|
80
|
+
*/
|
|
81
|
+
export interface LinearConfig {
|
|
82
|
+
apiKey: string;
|
|
83
|
+
defaultTeamId?: string;
|
|
84
|
+
defaultTeamKey?: string;
|
|
85
|
+
organizationName?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Filters for querying Linear issues.
|
|
89
|
+
*/
|
|
90
|
+
export interface LinearIssueFilter {
|
|
91
|
+
teamId?: string;
|
|
92
|
+
teamKey?: string;
|
|
93
|
+
stateType?: string;
|
|
94
|
+
stateName?: string;
|
|
95
|
+
assigneeId?: string;
|
|
96
|
+
assigneeMe?: boolean;
|
|
97
|
+
labelName?: string;
|
|
98
|
+
cycleId?: string;
|
|
99
|
+
projectId?: string;
|
|
100
|
+
search?: string;
|
|
101
|
+
limit?: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Mapping record between a Linear issue and a PMO ticket.
|
|
105
|
+
* Stored in pmo_linear_issue_map table.
|
|
106
|
+
*/
|
|
107
|
+
export interface LinearIssueMap {
|
|
108
|
+
pmoTicketId: string;
|
|
109
|
+
linearIssueId: string;
|
|
110
|
+
linearIdentifier: string;
|
|
111
|
+
linearTeamKey: string;
|
|
112
|
+
linearUrl: string;
|
|
113
|
+
syncDirection: 'inbound' | 'outbound' | 'bidirectional';
|
|
114
|
+
lastSyncedAt?: Date;
|
|
115
|
+
createdAt: Date;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Result of a sync operation.
|
|
119
|
+
*/
|
|
120
|
+
export interface LinearSyncResult {
|
|
121
|
+
imported: number;
|
|
122
|
+
updated: number;
|
|
123
|
+
skipped: number;
|
|
124
|
+
errors: Array<{
|
|
125
|
+
identifier: string;
|
|
126
|
+
error: string;
|
|
127
|
+
}>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Status mapping between Linear state types and PMO state categories.
|
|
131
|
+
*/
|
|
132
|
+
export declare const LINEAR_STATE_TO_PMO_CATEGORY: Record<string, string>;
|
|
133
|
+
/**
|
|
134
|
+
* Priority mapping between Linear (1-4) and PMO (P0-P3).
|
|
135
|
+
* Linear: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low
|
|
136
|
+
* PMO: P0=Critical, P1=High, P2=Medium, P3=Low
|
|
137
|
+
*/
|
|
138
|
+
export declare const LINEAR_PRIORITY_TO_PMO: Record<number, string>;
|
|
139
|
+
export declare const PMO_PRIORITY_TO_LINEAR: Record<string, number>;
|