prjct-cli 0.40.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/integrations/jira/cache.ts +57 -0
- package/core/integrations/jira/index.ts +16 -11
- package/core/integrations/jira/service.ts +244 -0
- package/core/integrations/linear/cache.ts +68 -0
- package/core/integrations/linear/index.ts +15 -1
- package/core/integrations/linear/service.ts +260 -0
- package/dist/bin/prjct.mjs +17 -6
- package/dist/core/infrastructure/setup.js +192 -238
- package/package.json +1 -1
- package/templates/commands/done.md +24 -1
- package/templates/commands/jira.md +91 -139
- package/templates/commands/linear.md +81 -130
- package/templates/commands/task.md +66 -2
- package/templates/mcp-config.json +4 -20
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA Cache Module
|
|
3
|
+
* 5-minute TTL cache for JIRA API responses to reduce API calls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TTLCache } from '../../utils/cache'
|
|
7
|
+
import type { Issue } from '../issue-tracker/types'
|
|
8
|
+
|
|
9
|
+
// 5-minute TTL for JIRA API responses
|
|
10
|
+
const JIRA_CACHE_TTL = 5 * 60 * 1000 // 300000ms
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cache for individual issues (by key like "ENG-123" or ID)
|
|
14
|
+
* Key format: "issue:{key}" or "issue:{id}"
|
|
15
|
+
*/
|
|
16
|
+
export const issueCache = new TTLCache<Issue>({
|
|
17
|
+
ttl: JIRA_CACHE_TTL,
|
|
18
|
+
maxSize: 100,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Cache for assigned issues list
|
|
23
|
+
* Key format: "assigned:{userId}" or "assigned:me"
|
|
24
|
+
*/
|
|
25
|
+
export const assignedIssuesCache = new TTLCache<Issue[]>({
|
|
26
|
+
ttl: JIRA_CACHE_TTL,
|
|
27
|
+
maxSize: 10,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cache for projects list
|
|
32
|
+
* Key format: "projects"
|
|
33
|
+
*/
|
|
34
|
+
export const projectsCache = new TTLCache<Array<{ id: string; name: string; key?: string }>>({
|
|
35
|
+
ttl: JIRA_CACHE_TTL,
|
|
36
|
+
maxSize: 5,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Clear all JIRA caches
|
|
41
|
+
*/
|
|
42
|
+
export function clearJiraCache(): void {
|
|
43
|
+
issueCache.clear()
|
|
44
|
+
assignedIssuesCache.clear()
|
|
45
|
+
projectsCache.clear()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get cache statistics for debugging
|
|
50
|
+
*/
|
|
51
|
+
export function getJiraCacheStats() {
|
|
52
|
+
return {
|
|
53
|
+
issues: issueCache.stats(),
|
|
54
|
+
assignedIssues: assignedIssuesCache.stats(),
|
|
55
|
+
projects: projectsCache.stats(),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JIRA Integration
|
|
3
3
|
*
|
|
4
|
-
* Provides JIRA issue tracking integration for prjct-cli.
|
|
4
|
+
* Provides JIRA issue tracking integration for prjct-cli using REST API.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 1. API Token Mode (direct REST API):
|
|
6
|
+
* Authentication (API Token Mode):
|
|
9
7
|
* - JIRA_BASE_URL: Your JIRA instance URL (e.g., https://company.atlassian.net)
|
|
10
8
|
* - JIRA_EMAIL: Your Atlassian account email
|
|
11
9
|
* - JIRA_API_TOKEN: API token from https://id.atlassian.com/manage-profile/security/api-tokens
|
|
12
|
-
*
|
|
13
|
-
* 2. MCP Mode (for corporate SSO):
|
|
14
|
-
* - No environment variables needed
|
|
15
|
-
* - Requires Atlassian MCP server configured in ~/.claude/mcp.json
|
|
16
|
-
* - Authenticates via browser (OAuth 2.1, SSO compatible)
|
|
17
10
|
*/
|
|
18
11
|
|
|
19
|
-
// REST API client
|
|
12
|
+
// REST API client
|
|
20
13
|
export { JiraProvider, jiraProvider, type JiraAuthMode } from './client'
|
|
21
14
|
|
|
22
|
-
//
|
|
15
|
+
// Service layer with caching (preferred API)
|
|
16
|
+
export { JiraService, jiraService } from './service'
|
|
17
|
+
|
|
18
|
+
// Cache utilities
|
|
19
|
+
export {
|
|
20
|
+
issueCache,
|
|
21
|
+
assignedIssuesCache,
|
|
22
|
+
projectsCache,
|
|
23
|
+
clearJiraCache,
|
|
24
|
+
getJiraCacheStats,
|
|
25
|
+
} from './cache'
|
|
26
|
+
|
|
27
|
+
// MCP adapter (deprecated - will be removed)
|
|
23
28
|
export {
|
|
24
29
|
JiraMCPAdapter,
|
|
25
30
|
jiraMCPAdapter,
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA Service Layer
|
|
3
|
+
* Wraps JiraProvider with caching for improved performance.
|
|
4
|
+
* All operations are cached with 5-minute TTL.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { jiraProvider } from './client'
|
|
8
|
+
import {
|
|
9
|
+
issueCache,
|
|
10
|
+
assignedIssuesCache,
|
|
11
|
+
projectsCache,
|
|
12
|
+
clearJiraCache,
|
|
13
|
+
getJiraCacheStats,
|
|
14
|
+
} from './cache'
|
|
15
|
+
import type {
|
|
16
|
+
Issue,
|
|
17
|
+
CreateIssueInput,
|
|
18
|
+
UpdateIssueInput,
|
|
19
|
+
FetchOptions,
|
|
20
|
+
JiraConfig,
|
|
21
|
+
} from '../issue-tracker/types'
|
|
22
|
+
|
|
23
|
+
export class JiraService {
|
|
24
|
+
private initialized = false
|
|
25
|
+
private userId: string | null = null
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if service is ready
|
|
29
|
+
*/
|
|
30
|
+
isReady(): boolean {
|
|
31
|
+
return this.initialized && jiraProvider.isConfigured()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize the service with config
|
|
36
|
+
* Must be called before any operations
|
|
37
|
+
*/
|
|
38
|
+
async initialize(config: JiraConfig): Promise<void> {
|
|
39
|
+
if (this.initialized) return
|
|
40
|
+
|
|
41
|
+
await jiraProvider.initialize(config)
|
|
42
|
+
this.initialized = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Initialize from credentials directly
|
|
47
|
+
* Convenience method for simple setup
|
|
48
|
+
*/
|
|
49
|
+
async initializeFromCredentials(
|
|
50
|
+
baseUrl: string,
|
|
51
|
+
email: string,
|
|
52
|
+
apiToken: string,
|
|
53
|
+
projectKey?: string
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
// Set env vars for the provider
|
|
56
|
+
process.env.JIRA_BASE_URL = baseUrl
|
|
57
|
+
process.env.JIRA_EMAIL = email
|
|
58
|
+
process.env.JIRA_API_TOKEN = apiToken
|
|
59
|
+
|
|
60
|
+
const config: JiraConfig = {
|
|
61
|
+
enabled: true,
|
|
62
|
+
provider: 'jira',
|
|
63
|
+
baseUrl,
|
|
64
|
+
projectKey,
|
|
65
|
+
syncOn: { task: true, done: true, ship: true },
|
|
66
|
+
enrichment: { enabled: true, updateProvider: true },
|
|
67
|
+
}
|
|
68
|
+
await this.initialize(config)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get issues assigned to current user (cached)
|
|
73
|
+
*/
|
|
74
|
+
async fetchAssignedIssues(options?: FetchOptions): Promise<Issue[]> {
|
|
75
|
+
this.ensureInitialized()
|
|
76
|
+
|
|
77
|
+
const cacheKey = `assigned:${this.userId || 'me'}`
|
|
78
|
+
const cached = assignedIssuesCache.get(cacheKey)
|
|
79
|
+
if (cached) {
|
|
80
|
+
return cached
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const issues = await jiraProvider.fetchAssignedIssues(options)
|
|
84
|
+
assignedIssuesCache.set(cacheKey, issues)
|
|
85
|
+
|
|
86
|
+
// Also cache individual issues
|
|
87
|
+
for (const issue of issues) {
|
|
88
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
89
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return issues
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get issues from a project (cached)
|
|
97
|
+
*/
|
|
98
|
+
async fetchProjectIssues(projectKey: string, options?: FetchOptions): Promise<Issue[]> {
|
|
99
|
+
this.ensureInitialized()
|
|
100
|
+
|
|
101
|
+
const cacheKey = `project:${projectKey}`
|
|
102
|
+
const cached = assignedIssuesCache.get(cacheKey)
|
|
103
|
+
if (cached) {
|
|
104
|
+
return cached
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const issues = await jiraProvider.fetchTeamIssues(projectKey, options)
|
|
108
|
+
assignedIssuesCache.set(cacheKey, issues)
|
|
109
|
+
|
|
110
|
+
// Also cache individual issues
|
|
111
|
+
for (const issue of issues) {
|
|
112
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
113
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return issues
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get a single issue by key (like "ENG-123") or ID (cached)
|
|
121
|
+
*/
|
|
122
|
+
async fetchIssue(id: string): Promise<Issue | null> {
|
|
123
|
+
this.ensureInitialized()
|
|
124
|
+
|
|
125
|
+
// Check cache first
|
|
126
|
+
const cacheKey = `issue:${id}`
|
|
127
|
+
const cached = issueCache.get(cacheKey)
|
|
128
|
+
if (cached) {
|
|
129
|
+
return cached
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const issue = await jiraProvider.fetchIssue(id)
|
|
133
|
+
if (issue) {
|
|
134
|
+
// Cache by both ID and key
|
|
135
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
136
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return issue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a new issue (invalidates assigned cache)
|
|
144
|
+
*/
|
|
145
|
+
async createIssue(input: CreateIssueInput): Promise<Issue> {
|
|
146
|
+
this.ensureInitialized()
|
|
147
|
+
|
|
148
|
+
const issue = await jiraProvider.createIssue(input)
|
|
149
|
+
|
|
150
|
+
// Cache the new issue
|
|
151
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
152
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
153
|
+
|
|
154
|
+
// Invalidate assigned issues cache (new issue may be assigned)
|
|
155
|
+
assignedIssuesCache.clear()
|
|
156
|
+
|
|
157
|
+
return issue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Update an issue (invalidates cache for that issue)
|
|
162
|
+
*/
|
|
163
|
+
async updateIssue(id: string, input: UpdateIssueInput): Promise<Issue> {
|
|
164
|
+
this.ensureInitialized()
|
|
165
|
+
|
|
166
|
+
const issue = await jiraProvider.updateIssue(id, input)
|
|
167
|
+
|
|
168
|
+
// Update cache
|
|
169
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
170
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
171
|
+
|
|
172
|
+
return issue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Mark issue as in progress (invalidates cache)
|
|
177
|
+
*/
|
|
178
|
+
async markInProgress(id: string): Promise<void> {
|
|
179
|
+
this.ensureInitialized()
|
|
180
|
+
|
|
181
|
+
await jiraProvider.markInProgress(id)
|
|
182
|
+
|
|
183
|
+
// Invalidate caches
|
|
184
|
+
issueCache.delete(`issue:${id}`)
|
|
185
|
+
assignedIssuesCache.clear()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Mark issue as done (invalidates cache)
|
|
190
|
+
*/
|
|
191
|
+
async markDone(id: string): Promise<void> {
|
|
192
|
+
this.ensureInitialized()
|
|
193
|
+
|
|
194
|
+
await jiraProvider.markDone(id)
|
|
195
|
+
|
|
196
|
+
// Invalidate caches
|
|
197
|
+
issueCache.delete(`issue:${id}`)
|
|
198
|
+
assignedIssuesCache.clear()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get available projects (cached)
|
|
203
|
+
*/
|
|
204
|
+
async getProjects(): Promise<Array<{ id: string; name: string; key?: string }>> {
|
|
205
|
+
this.ensureInitialized()
|
|
206
|
+
|
|
207
|
+
const cached = projectsCache.get('projects')
|
|
208
|
+
if (cached) {
|
|
209
|
+
return cached
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const projects = await jiraProvider.getTeams()
|
|
213
|
+
projectsCache.set('projects', projects)
|
|
214
|
+
return projects
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Clear all caches
|
|
219
|
+
*/
|
|
220
|
+
clearCache(): void {
|
|
221
|
+
clearJiraCache()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get cache statistics for debugging
|
|
226
|
+
*/
|
|
227
|
+
getCacheStats() {
|
|
228
|
+
return getJiraCacheStats()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Ensure service is initialized
|
|
233
|
+
*/
|
|
234
|
+
private ensureInitialized(): void {
|
|
235
|
+
if (!this.initialized) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
'JIRA service not initialized. Call jiraService.initialize() first or run `p. jira setup`.'
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Singleton instance
|
|
244
|
+
export const jiraService = new JiraService()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Cache Module
|
|
3
|
+
* 5-minute TTL cache for Linear API responses to reduce API calls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TTLCache } from '../../utils/cache'
|
|
7
|
+
import type { Issue } from '../issue-tracker/types'
|
|
8
|
+
|
|
9
|
+
// 5-minute TTL for Linear API responses
|
|
10
|
+
const LINEAR_CACHE_TTL = 5 * 60 * 1000 // 300000ms
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cache for individual issues (by ID or identifier)
|
|
14
|
+
* Key format: "issue:{id}" or "issue:{identifier}"
|
|
15
|
+
*/
|
|
16
|
+
export const issueCache = new TTLCache<Issue>({
|
|
17
|
+
ttl: LINEAR_CACHE_TTL,
|
|
18
|
+
maxSize: 100,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Cache for assigned issues list
|
|
23
|
+
* Key format: "assigned:{userId}" or "assigned:me"
|
|
24
|
+
*/
|
|
25
|
+
export const assignedIssuesCache = new TTLCache<Issue[]>({
|
|
26
|
+
ttl: LINEAR_CACHE_TTL,
|
|
27
|
+
maxSize: 10,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cache for teams list
|
|
32
|
+
* Key format: "teams"
|
|
33
|
+
*/
|
|
34
|
+
export const teamsCache = new TTLCache<Array<{ id: string; name: string; key?: string }>>({
|
|
35
|
+
ttl: LINEAR_CACHE_TTL,
|
|
36
|
+
maxSize: 5,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cache for projects list
|
|
41
|
+
* Key format: "projects"
|
|
42
|
+
*/
|
|
43
|
+
export const projectsCache = new TTLCache<Array<{ id: string; name: string }>>({
|
|
44
|
+
ttl: LINEAR_CACHE_TTL,
|
|
45
|
+
maxSize: 5,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Clear all Linear caches
|
|
50
|
+
*/
|
|
51
|
+
export function clearLinearCache(): void {
|
|
52
|
+
issueCache.clear()
|
|
53
|
+
assignedIssuesCache.clear()
|
|
54
|
+
teamsCache.clear()
|
|
55
|
+
projectsCache.clear()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get cache statistics for debugging
|
|
60
|
+
*/
|
|
61
|
+
export function getLinearCacheStats() {
|
|
62
|
+
return {
|
|
63
|
+
issues: issueCache.stats(),
|
|
64
|
+
assignedIssues: assignedIssuesCache.stats(),
|
|
65
|
+
teams: teamsCache.stats(),
|
|
66
|
+
projects: projectsCache.stats(),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Linear Integration
|
|
3
|
-
* Issue tracker provider for Linear
|
|
3
|
+
* Issue tracker provider for Linear using @linear/sdk
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// Core provider
|
|
6
7
|
export { LinearProvider, linearProvider } from './client'
|
|
8
|
+
|
|
9
|
+
// Service layer with caching (preferred API)
|
|
10
|
+
export { LinearService, linearService } from './service'
|
|
11
|
+
|
|
12
|
+
// Cache utilities
|
|
13
|
+
export {
|
|
14
|
+
issueCache,
|
|
15
|
+
assignedIssuesCache,
|
|
16
|
+
teamsCache,
|
|
17
|
+
projectsCache,
|
|
18
|
+
clearLinearCache,
|
|
19
|
+
getLinearCacheStats,
|
|
20
|
+
} from './cache'
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Service Layer
|
|
3
|
+
* Wraps LinearProvider with caching for improved performance.
|
|
4
|
+
* All operations are cached with 5-minute TTL.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { linearProvider } from './client'
|
|
8
|
+
import {
|
|
9
|
+
issueCache,
|
|
10
|
+
assignedIssuesCache,
|
|
11
|
+
teamsCache,
|
|
12
|
+
projectsCache,
|
|
13
|
+
clearLinearCache,
|
|
14
|
+
getLinearCacheStats,
|
|
15
|
+
} from './cache'
|
|
16
|
+
import type {
|
|
17
|
+
Issue,
|
|
18
|
+
CreateIssueInput,
|
|
19
|
+
UpdateIssueInput,
|
|
20
|
+
FetchOptions,
|
|
21
|
+
LinearConfig,
|
|
22
|
+
} from '../issue-tracker/types'
|
|
23
|
+
|
|
24
|
+
export class LinearService {
|
|
25
|
+
private initialized = false
|
|
26
|
+
private userId: string | null = null
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if service is ready
|
|
30
|
+
*/
|
|
31
|
+
isReady(): boolean {
|
|
32
|
+
return this.initialized && linearProvider.isConfigured()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize the service with config
|
|
37
|
+
* Must be called before any operations
|
|
38
|
+
*/
|
|
39
|
+
async initialize(config: LinearConfig): Promise<void> {
|
|
40
|
+
if (this.initialized) return
|
|
41
|
+
|
|
42
|
+
await linearProvider.initialize(config)
|
|
43
|
+
this.initialized = true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize from API key directly
|
|
48
|
+
* Convenience method for simple setup
|
|
49
|
+
*/
|
|
50
|
+
async initializeFromApiKey(apiKey: string, teamId?: string): Promise<void> {
|
|
51
|
+
const config: LinearConfig = {
|
|
52
|
+
enabled: true,
|
|
53
|
+
provider: 'linear',
|
|
54
|
+
apiKey,
|
|
55
|
+
defaultTeamId: teamId,
|
|
56
|
+
syncOn: { task: true, done: true, ship: true },
|
|
57
|
+
enrichment: { enabled: true, updateProvider: true },
|
|
58
|
+
}
|
|
59
|
+
await this.initialize(config)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get issues assigned to current user (cached)
|
|
64
|
+
*/
|
|
65
|
+
async fetchAssignedIssues(options?: FetchOptions): Promise<Issue[]> {
|
|
66
|
+
this.ensureInitialized()
|
|
67
|
+
|
|
68
|
+
const cacheKey = `assigned:${this.userId || 'me'}`
|
|
69
|
+
const cached = assignedIssuesCache.get(cacheKey)
|
|
70
|
+
if (cached) {
|
|
71
|
+
return cached
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const issues = await linearProvider.fetchAssignedIssues(options)
|
|
75
|
+
assignedIssuesCache.set(cacheKey, issues)
|
|
76
|
+
|
|
77
|
+
// Also cache individual issues
|
|
78
|
+
for (const issue of issues) {
|
|
79
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
80
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return issues
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get issues from a team (cached)
|
|
88
|
+
*/
|
|
89
|
+
async fetchTeamIssues(teamId: string, options?: FetchOptions): Promise<Issue[]> {
|
|
90
|
+
this.ensureInitialized()
|
|
91
|
+
|
|
92
|
+
const cacheKey = `team:${teamId}`
|
|
93
|
+
const cached = assignedIssuesCache.get(cacheKey)
|
|
94
|
+
if (cached) {
|
|
95
|
+
return cached
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const issues = await linearProvider.fetchTeamIssues(teamId, options)
|
|
99
|
+
assignedIssuesCache.set(cacheKey, issues)
|
|
100
|
+
|
|
101
|
+
// Also cache individual issues
|
|
102
|
+
for (const issue of issues) {
|
|
103
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
104
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return issues
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get a single issue by ID or identifier (cached)
|
|
112
|
+
* Accepts UUID or identifier like "PRJ-123"
|
|
113
|
+
*/
|
|
114
|
+
async fetchIssue(id: string): Promise<Issue | null> {
|
|
115
|
+
this.ensureInitialized()
|
|
116
|
+
|
|
117
|
+
// Check cache first
|
|
118
|
+
const cacheKey = `issue:${id}`
|
|
119
|
+
const cached = issueCache.get(cacheKey)
|
|
120
|
+
if (cached) {
|
|
121
|
+
return cached
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const issue = await linearProvider.fetchIssue(id)
|
|
125
|
+
if (issue) {
|
|
126
|
+
// Cache by both ID and externalId
|
|
127
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
128
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return issue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a new issue (invalidates assigned cache)
|
|
136
|
+
*/
|
|
137
|
+
async createIssue(input: CreateIssueInput): Promise<Issue> {
|
|
138
|
+
this.ensureInitialized()
|
|
139
|
+
|
|
140
|
+
const issue = await linearProvider.createIssue(input)
|
|
141
|
+
|
|
142
|
+
// Cache the new issue
|
|
143
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
144
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
145
|
+
|
|
146
|
+
// Invalidate assigned issues cache (new issue may be assigned)
|
|
147
|
+
assignedIssuesCache.clear()
|
|
148
|
+
|
|
149
|
+
return issue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Update an issue (invalidates cache for that issue)
|
|
154
|
+
*/
|
|
155
|
+
async updateIssue(id: string, input: UpdateIssueInput): Promise<Issue> {
|
|
156
|
+
this.ensureInitialized()
|
|
157
|
+
|
|
158
|
+
const issue = await linearProvider.updateIssue(id, input)
|
|
159
|
+
|
|
160
|
+
// Update cache
|
|
161
|
+
issueCache.set(`issue:${issue.id}`, issue)
|
|
162
|
+
issueCache.set(`issue:${issue.externalId}`, issue)
|
|
163
|
+
|
|
164
|
+
return issue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Mark issue as in progress (invalidates cache)
|
|
169
|
+
*/
|
|
170
|
+
async markInProgress(id: string): Promise<void> {
|
|
171
|
+
this.ensureInitialized()
|
|
172
|
+
|
|
173
|
+
await linearProvider.markInProgress(id)
|
|
174
|
+
|
|
175
|
+
// Invalidate caches
|
|
176
|
+
issueCache.delete(`issue:${id}`)
|
|
177
|
+
assignedIssuesCache.clear()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Mark issue as done (invalidates cache)
|
|
182
|
+
*/
|
|
183
|
+
async markDone(id: string): Promise<void> {
|
|
184
|
+
this.ensureInitialized()
|
|
185
|
+
|
|
186
|
+
await linearProvider.markDone(id)
|
|
187
|
+
|
|
188
|
+
// Invalidate caches
|
|
189
|
+
issueCache.delete(`issue:${id}`)
|
|
190
|
+
assignedIssuesCache.clear()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Add a comment to an issue
|
|
195
|
+
*/
|
|
196
|
+
async addComment(id: string, body: string): Promise<void> {
|
|
197
|
+
this.ensureInitialized()
|
|
198
|
+
await linearProvider.addComment(id, body)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get available teams (cached)
|
|
203
|
+
*/
|
|
204
|
+
async getTeams(): Promise<Array<{ id: string; name: string; key?: string }>> {
|
|
205
|
+
this.ensureInitialized()
|
|
206
|
+
|
|
207
|
+
const cached = teamsCache.get('teams')
|
|
208
|
+
if (cached) {
|
|
209
|
+
return cached
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const teams = await linearProvider.getTeams()
|
|
213
|
+
teamsCache.set('teams', teams)
|
|
214
|
+
return teams
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get available projects (cached)
|
|
219
|
+
*/
|
|
220
|
+
async getProjects(): Promise<Array<{ id: string; name: string }>> {
|
|
221
|
+
this.ensureInitialized()
|
|
222
|
+
|
|
223
|
+
const cached = projectsCache.get('projects')
|
|
224
|
+
if (cached) {
|
|
225
|
+
return cached
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const projects = await linearProvider.getProjects()
|
|
229
|
+
projectsCache.set('projects', projects)
|
|
230
|
+
return projects
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Clear all caches
|
|
235
|
+
*/
|
|
236
|
+
clearCache(): void {
|
|
237
|
+
clearLinearCache()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get cache statistics for debugging
|
|
242
|
+
*/
|
|
243
|
+
getCacheStats() {
|
|
244
|
+
return getLinearCacheStats()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Ensure service is initialized
|
|
249
|
+
*/
|
|
250
|
+
private ensureInitialized(): void {
|
|
251
|
+
if (!this.initialized) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
'Linear service not initialized. Call linearService.initialize() first or run `p. linear setup`.'
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Singleton instance
|
|
260
|
+
export const linearService = new LinearService()
|