prjct-cli 0.41.0 → 0.42.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/CHANGELOG.md +37 -0
- package/bin/prjct.ts +26 -0
- package/core/cli/linear.ts +381 -0
- package/core/utils/project-credentials.ts +148 -0
- package/dist/bin/prjct.mjs +19 -1
- package/dist/core/infrastructure/setup.js +238 -192
- package/package.json +1 -1
- package/templates/commands/enrich.md +152 -18
- package/templates/commands/linear.md +169 -135
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.42.0] - 2026-01-29
|
|
4
|
+
|
|
5
|
+
### Feature: Linear SDK Integration with Per-Project Credentials
|
|
6
|
+
|
|
7
|
+
Linear integration now uses the native `@linear/sdk` with per-project credential storage, enabling different projects to use different Linear workspaces.
|
|
8
|
+
|
|
9
|
+
**New: Per-Project Credentials**
|
|
10
|
+
- Credentials stored at `~/.prjct-cli/projects/{projectId}/config/credentials.json`
|
|
11
|
+
- Fallback chain: project credentials → macOS keychain → environment variable
|
|
12
|
+
- Each project can connect to a different Linear workspace
|
|
13
|
+
|
|
14
|
+
**New: CLI Bridge (`core/cli/linear.ts`)**
|
|
15
|
+
- Direct access to Linear SDK from templates
|
|
16
|
+
- Commands: `setup`, `list`, `get`, `create`, `update`, `start`, `done`, `comment`, `teams`, `projects`, `status`
|
|
17
|
+
- JSON output for easy parsing by Claude
|
|
18
|
+
|
|
19
|
+
**New: `prjct linear <cmd>` Subcommand**
|
|
20
|
+
- Direct CLI access: `prjct linear status`, `prjct linear list`, etc.
|
|
21
|
+
- Auto-resolves project ID from current directory
|
|
22
|
+
|
|
23
|
+
**Updated Templates**
|
|
24
|
+
- `linear.md` — Natural language interpretation guide for Claude
|
|
25
|
+
- `enrich.md` — Uses CLI instead of MCP for ticket enrichment
|
|
26
|
+
|
|
27
|
+
**Breaking: Removed MCP-only Trackers**
|
|
28
|
+
- Removed JIRA, GitHub Issues, Monday.com support (no native SDK)
|
|
29
|
+
- Only Linear is supported (has native SDK)
|
|
30
|
+
|
|
31
|
+
**New Files:**
|
|
32
|
+
- `core/cli/linear.ts` — CLI bridge for Linear SDK
|
|
33
|
+
- `core/utils/project-credentials.ts` — Per-project credential storage
|
|
34
|
+
|
|
35
|
+
**Modified:**
|
|
36
|
+
- `bin/prjct.ts` — Added `prjct linear` subcommand
|
|
37
|
+
- `templates/commands/linear.md` — Updated with CLI execution pattern
|
|
38
|
+
- `templates/commands/enrich.md` — Linear-only, uses CLI
|
|
39
|
+
|
|
3
40
|
## [0.40.0] - 2026-01-28
|
|
4
41
|
|
|
5
42
|
### Feature: Enhanced Skill System
|
package/bin/prjct.ts
CHANGED
|
@@ -85,6 +85,32 @@ if (args[0] === 'start' || args[0] === 'setup') {
|
|
|
85
85
|
console.error('Server error:', (error as Error).message)
|
|
86
86
|
process.exitCode = 1
|
|
87
87
|
}
|
|
88
|
+
} else if (args[0] === 'linear') {
|
|
89
|
+
// Linear CLI subcommand - direct access to Linear SDK
|
|
90
|
+
const { spawn } = await import('child_process')
|
|
91
|
+
const projectPath = process.cwd()
|
|
92
|
+
const projectId = await configManager.getProjectId(projectPath)
|
|
93
|
+
|
|
94
|
+
if (!projectId) {
|
|
95
|
+
console.error('No prjct project found. Run "prjct init" first.')
|
|
96
|
+
process.exitCode = 1
|
|
97
|
+
} else {
|
|
98
|
+
// Get the path to the linear CLI
|
|
99
|
+
const linearCliPath = path.join(__dirname, '..', 'core', 'cli', 'linear.ts')
|
|
100
|
+
|
|
101
|
+
// Forward args to linear CLI, adding --project flag
|
|
102
|
+
const linearArgs = ['--project', projectId, ...args.slice(1)]
|
|
103
|
+
|
|
104
|
+
// Use bun to run the CLI
|
|
105
|
+
const child = spawn('bun', [linearCliPath, ...linearArgs], {
|
|
106
|
+
stdio: 'inherit',
|
|
107
|
+
cwd: projectPath,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
child.on('close', (code) => {
|
|
111
|
+
process.exitCode = code || 0
|
|
112
|
+
})
|
|
113
|
+
}
|
|
88
114
|
} else if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
|
|
89
115
|
// Show version with provider status
|
|
90
116
|
const detection = detectAllProviders()
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Linear CLI - Bridge between templates and SDK
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun core/cli/linear.ts --project <projectId> <command> [args...]
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* setup <apiKey> [teamId] - Store API key in project credentials
|
|
9
|
+
* list - List my assigned issues
|
|
10
|
+
* list-team <teamId> - List issues from a team
|
|
11
|
+
* get <id> - Get issue by ID or identifier (PRJ-123)
|
|
12
|
+
* create <json> - Create issue from JSON input
|
|
13
|
+
* update <id> <json> - Update issue
|
|
14
|
+
* start <id> - Mark issue as in progress
|
|
15
|
+
* done <id> - Mark issue as done
|
|
16
|
+
* comment <id> <text> - Add comment to issue
|
|
17
|
+
* teams - List available teams
|
|
18
|
+
* projects - List available projects
|
|
19
|
+
* status - Check connection status
|
|
20
|
+
*
|
|
21
|
+
* All output is JSON for easy parsing by Claude.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { linearService } from '../integrations/linear'
|
|
25
|
+
import {
|
|
26
|
+
getLinearApiKey,
|
|
27
|
+
setLinearCredentials,
|
|
28
|
+
getProjectCredentials,
|
|
29
|
+
getCredentialSource,
|
|
30
|
+
} from '../utils/project-credentials'
|
|
31
|
+
|
|
32
|
+
// Parse arguments
|
|
33
|
+
const args = process.argv.slice(2)
|
|
34
|
+
|
|
35
|
+
// Extract --project flag
|
|
36
|
+
const projectIdx = args.indexOf('--project')
|
|
37
|
+
let projectId: string | null = null
|
|
38
|
+
if (projectIdx !== -1 && args[projectIdx + 1]) {
|
|
39
|
+
projectId = args[projectIdx + 1]
|
|
40
|
+
args.splice(projectIdx, 2)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [command, ...commandArgs] = args
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Output result as JSON
|
|
47
|
+
*/
|
|
48
|
+
function output(data: unknown): void {
|
|
49
|
+
console.log(JSON.stringify(data, null, 2))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Output error as JSON and exit
|
|
54
|
+
*/
|
|
55
|
+
function error(message: string, code = 1): never {
|
|
56
|
+
console.error(JSON.stringify({ error: message }))
|
|
57
|
+
process.exit(code)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Initialize Linear service from project credentials
|
|
62
|
+
*/
|
|
63
|
+
async function initFromProject(): Promise<void> {
|
|
64
|
+
if (!projectId) {
|
|
65
|
+
error('No --project specified. Usage: linear.ts --project <projectId> <command>')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const apiKey = await getLinearApiKey(projectId)
|
|
69
|
+
if (!apiKey) {
|
|
70
|
+
error('Linear not configured. Run: p. linear setup')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const creds = await getProjectCredentials(projectId)
|
|
74
|
+
await linearService.initializeFromApiKey(apiKey, creds.linear?.teamId)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Main CLI handler
|
|
79
|
+
*/
|
|
80
|
+
async function main(): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
switch (command) {
|
|
83
|
+
case 'setup': {
|
|
84
|
+
if (!projectId) {
|
|
85
|
+
error('--project required for setup')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const apiKey = commandArgs[0]
|
|
89
|
+
const teamId = commandArgs[1] // optional
|
|
90
|
+
|
|
91
|
+
if (!apiKey) {
|
|
92
|
+
error('API key required. Usage: setup <apiKey> [teamId]')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Test connection first
|
|
96
|
+
await linearService.initializeFromApiKey(apiKey, teamId)
|
|
97
|
+
const teams = await linearService.getTeams()
|
|
98
|
+
|
|
99
|
+
if (teams.length === 0) {
|
|
100
|
+
error('No teams found. Check your API key permissions.')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Determine default team
|
|
104
|
+
let selectedTeamId = teamId
|
|
105
|
+
let selectedTeamKey: string | undefined
|
|
106
|
+
|
|
107
|
+
if (!selectedTeamId && teams.length === 1) {
|
|
108
|
+
selectedTeamId = teams[0].id
|
|
109
|
+
selectedTeamKey = teams[0].key
|
|
110
|
+
} else if (selectedTeamId) {
|
|
111
|
+
const team = teams.find((t) => t.id === selectedTeamId || t.key === selectedTeamId)
|
|
112
|
+
if (team) {
|
|
113
|
+
selectedTeamId = team.id
|
|
114
|
+
selectedTeamKey = team.key
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Store in project credentials
|
|
119
|
+
await setLinearCredentials(projectId, {
|
|
120
|
+
apiKey,
|
|
121
|
+
teamId: selectedTeamId,
|
|
122
|
+
teamKey: selectedTeamKey,
|
|
123
|
+
setupAt: new Date().toISOString(),
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
output({
|
|
127
|
+
success: true,
|
|
128
|
+
teams,
|
|
129
|
+
defaultTeam: selectedTeamId
|
|
130
|
+
? { id: selectedTeamId, key: selectedTeamKey }
|
|
131
|
+
: null,
|
|
132
|
+
})
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'list': {
|
|
137
|
+
await initFromProject()
|
|
138
|
+
const limit = commandArgs[0] ? parseInt(commandArgs[0], 10) : 20
|
|
139
|
+
const issues = await linearService.fetchAssignedIssues({ limit })
|
|
140
|
+
output({
|
|
141
|
+
count: issues.length,
|
|
142
|
+
issues: issues.map((issue) => ({
|
|
143
|
+
id: issue.id,
|
|
144
|
+
identifier: issue.externalId,
|
|
145
|
+
title: issue.title,
|
|
146
|
+
status: issue.status,
|
|
147
|
+
priority: issue.priority,
|
|
148
|
+
url: issue.url,
|
|
149
|
+
})),
|
|
150
|
+
})
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case 'list-team': {
|
|
155
|
+
await initFromProject()
|
|
156
|
+
const teamId = commandArgs[0]
|
|
157
|
+
const limit = commandArgs[1] ? parseInt(commandArgs[1], 10) : 20
|
|
158
|
+
|
|
159
|
+
if (!teamId) {
|
|
160
|
+
error('Team ID required. Usage: list-team <teamId> [limit]')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const issues = await linearService.fetchTeamIssues(teamId, { limit })
|
|
164
|
+
output({
|
|
165
|
+
count: issues.length,
|
|
166
|
+
issues: issues.map((issue) => ({
|
|
167
|
+
id: issue.id,
|
|
168
|
+
identifier: issue.externalId,
|
|
169
|
+
title: issue.title,
|
|
170
|
+
status: issue.status,
|
|
171
|
+
priority: issue.priority,
|
|
172
|
+
url: issue.url,
|
|
173
|
+
})),
|
|
174
|
+
})
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'get': {
|
|
179
|
+
await initFromProject()
|
|
180
|
+
const id = commandArgs[0]
|
|
181
|
+
|
|
182
|
+
if (!id) {
|
|
183
|
+
error('Issue ID required. Usage: get <id>')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const issue = await linearService.fetchIssue(id)
|
|
187
|
+
if (!issue) {
|
|
188
|
+
error(`Issue not found: ${id}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
output(issue)
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case 'create': {
|
|
196
|
+
await initFromProject()
|
|
197
|
+
const inputJson = commandArgs[0]
|
|
198
|
+
|
|
199
|
+
if (!inputJson) {
|
|
200
|
+
error('JSON input required. Usage: create \'{"title":"...", "teamId":"..."}\'')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let input
|
|
204
|
+
try {
|
|
205
|
+
input = JSON.parse(inputJson)
|
|
206
|
+
} catch {
|
|
207
|
+
error(`Invalid JSON: ${inputJson}`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!input.title) {
|
|
211
|
+
error('title is required')
|
|
212
|
+
}
|
|
213
|
+
if (!input.teamId) {
|
|
214
|
+
// Try to use default team from credentials
|
|
215
|
+
const creds = await getProjectCredentials(projectId!)
|
|
216
|
+
if (creds.linear?.teamId) {
|
|
217
|
+
input.teamId = creds.linear.teamId
|
|
218
|
+
} else {
|
|
219
|
+
error('teamId is required (no default team configured)')
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const issue = await linearService.createIssue(input)
|
|
224
|
+
output(issue)
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 'update': {
|
|
229
|
+
await initFromProject()
|
|
230
|
+
const id = commandArgs[0]
|
|
231
|
+
const inputJson = commandArgs[1]
|
|
232
|
+
|
|
233
|
+
if (!id) {
|
|
234
|
+
error('Issue ID required. Usage: update <id> \'{"description":"..."}\'')
|
|
235
|
+
}
|
|
236
|
+
if (!inputJson) {
|
|
237
|
+
error('JSON input required. Usage: update <id> \'{"description":"..."}\'')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let input
|
|
241
|
+
try {
|
|
242
|
+
input = JSON.parse(inputJson)
|
|
243
|
+
} catch {
|
|
244
|
+
error(`Invalid JSON: ${inputJson}`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const issue = await linearService.updateIssue(id, input)
|
|
248
|
+
output(issue)
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case 'start': {
|
|
253
|
+
await initFromProject()
|
|
254
|
+
const id = commandArgs[0]
|
|
255
|
+
|
|
256
|
+
if (!id) {
|
|
257
|
+
error('Issue ID required. Usage: start <id>')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await linearService.markInProgress(id)
|
|
261
|
+
output({ success: true, id, status: 'in_progress' })
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case 'done': {
|
|
266
|
+
await initFromProject()
|
|
267
|
+
const id = commandArgs[0]
|
|
268
|
+
|
|
269
|
+
if (!id) {
|
|
270
|
+
error('Issue ID required. Usage: done <id>')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await linearService.markDone(id)
|
|
274
|
+
output({ success: true, id, status: 'done' })
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case 'comment': {
|
|
279
|
+
await initFromProject()
|
|
280
|
+
const id = commandArgs[0]
|
|
281
|
+
const body = commandArgs.slice(1).join(' ')
|
|
282
|
+
|
|
283
|
+
if (!id) {
|
|
284
|
+
error('Issue ID required. Usage: comment <id> <text>')
|
|
285
|
+
}
|
|
286
|
+
if (!body) {
|
|
287
|
+
error('Comment text required. Usage: comment <id> <text>')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
await linearService.addComment(id, body)
|
|
291
|
+
output({ success: true, id })
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case 'teams': {
|
|
296
|
+
await initFromProject()
|
|
297
|
+
const teams = await linearService.getTeams()
|
|
298
|
+
output({ count: teams.length, teams })
|
|
299
|
+
break
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'projects': {
|
|
303
|
+
await initFromProject()
|
|
304
|
+
const projects = await linearService.getProjects()
|
|
305
|
+
output({ count: projects.length, projects })
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case 'status': {
|
|
310
|
+
if (!projectId) {
|
|
311
|
+
error('--project required for status')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const source = await getCredentialSource(projectId)
|
|
315
|
+
const apiKey = await getLinearApiKey(projectId)
|
|
316
|
+
const creds = await getProjectCredentials(projectId)
|
|
317
|
+
|
|
318
|
+
if (!apiKey) {
|
|
319
|
+
output({
|
|
320
|
+
configured: false,
|
|
321
|
+
source: 'none',
|
|
322
|
+
message: 'Linear not configured. Run: p. linear setup',
|
|
323
|
+
})
|
|
324
|
+
break
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Test connection
|
|
328
|
+
try {
|
|
329
|
+
await linearService.initializeFromApiKey(apiKey, creds.linear?.teamId)
|
|
330
|
+
const teams = await linearService.getTeams()
|
|
331
|
+
|
|
332
|
+
output({
|
|
333
|
+
configured: true,
|
|
334
|
+
source,
|
|
335
|
+
teamId: creds.linear?.teamId,
|
|
336
|
+
teamKey: creds.linear?.teamKey,
|
|
337
|
+
teamsAvailable: teams.length,
|
|
338
|
+
})
|
|
339
|
+
} catch (err) {
|
|
340
|
+
output({
|
|
341
|
+
configured: true,
|
|
342
|
+
source,
|
|
343
|
+
connectionError: (err as Error).message,
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
break
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
case 'help':
|
|
350
|
+
case '--help':
|
|
351
|
+
case '-h':
|
|
352
|
+
case undefined: {
|
|
353
|
+
output({
|
|
354
|
+
usage: 'linear.ts --project <projectId> <command> [args...]',
|
|
355
|
+
commands: {
|
|
356
|
+
setup: 'setup <apiKey> [teamId] - Store API key',
|
|
357
|
+
list: 'list [limit] - List my assigned issues',
|
|
358
|
+
'list-team': 'list-team <teamId> [limit] - List team issues',
|
|
359
|
+
get: 'get <id> - Get issue by ID or identifier',
|
|
360
|
+
create: 'create <json> - Create issue',
|
|
361
|
+
update: 'update <id> <json> - Update issue',
|
|
362
|
+
start: 'start <id> - Mark as in progress',
|
|
363
|
+
done: 'done <id> - Mark as done',
|
|
364
|
+
comment: 'comment <id> <text> - Add comment',
|
|
365
|
+
teams: 'teams - List available teams',
|
|
366
|
+
projects: 'projects - List available projects',
|
|
367
|
+
status: 'status - Check connection',
|
|
368
|
+
},
|
|
369
|
+
})
|
|
370
|
+
break
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
default:
|
|
374
|
+
error(`Unknown command: ${command}. Use --help to see available commands.`)
|
|
375
|
+
}
|
|
376
|
+
} catch (err) {
|
|
377
|
+
error((err as Error).message)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
main()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project credential storage
|
|
3
|
+
*
|
|
4
|
+
* Stores credentials in: ~/.prjct-cli/projects/{projectId}/config/credentials.json
|
|
5
|
+
*
|
|
6
|
+
* Fallback chain for Linear API key:
|
|
7
|
+
* 1. Project credentials (per-project)
|
|
8
|
+
* 2. Global keychain (macOS)
|
|
9
|
+
* 3. Environment variables
|
|
10
|
+
*
|
|
11
|
+
* This allows different projects to use different Linear workspaces.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs'
|
|
15
|
+
import path from 'path'
|
|
16
|
+
import os from 'os'
|
|
17
|
+
import { getCredential, type CredentialKey } from './keychain'
|
|
18
|
+
|
|
19
|
+
interface LinearCredentials {
|
|
20
|
+
apiKey: string
|
|
21
|
+
teamId?: string
|
|
22
|
+
teamKey?: string
|
|
23
|
+
setupAt: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ProjectCredentials {
|
|
27
|
+
linear?: LinearCredentials
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get path to project credentials file
|
|
32
|
+
*/
|
|
33
|
+
function getCredentialsPath(projectId: string): string {
|
|
34
|
+
return path.join(
|
|
35
|
+
os.homedir(),
|
|
36
|
+
'.prjct-cli',
|
|
37
|
+
'projects',
|
|
38
|
+
projectId,
|
|
39
|
+
'config',
|
|
40
|
+
'credentials.json'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get all credentials for a project
|
|
46
|
+
*/
|
|
47
|
+
export async function getProjectCredentials(projectId: string): Promise<ProjectCredentials> {
|
|
48
|
+
const credPath = getCredentialsPath(projectId)
|
|
49
|
+
if (!fs.existsSync(credPath)) {
|
|
50
|
+
return {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(fs.readFileSync(credPath, 'utf-8'))
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('[project-credentials] Failed to read credentials:', (error as Error).message)
|
|
57
|
+
return {}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set Linear credentials for a project
|
|
63
|
+
*/
|
|
64
|
+
export async function setLinearCredentials(
|
|
65
|
+
projectId: string,
|
|
66
|
+
credentials: LinearCredentials
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
const credPath = getCredentialsPath(projectId)
|
|
69
|
+
const dir = path.dirname(credPath)
|
|
70
|
+
|
|
71
|
+
// Ensure directory exists
|
|
72
|
+
if (!fs.existsSync(dir)) {
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Read existing, merge, write
|
|
77
|
+
const current = await getProjectCredentials(projectId)
|
|
78
|
+
current.linear = credentials
|
|
79
|
+
fs.writeFileSync(credPath, JSON.stringify(current, null, 2))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Delete Linear credentials for a project
|
|
84
|
+
*/
|
|
85
|
+
export async function deleteLinearCredentials(projectId: string): Promise<void> {
|
|
86
|
+
const current = await getProjectCredentials(projectId)
|
|
87
|
+
|
|
88
|
+
if (current.linear) {
|
|
89
|
+
delete current.linear
|
|
90
|
+
const credPath = getCredentialsPath(projectId)
|
|
91
|
+
fs.writeFileSync(credPath, JSON.stringify(current, null, 2))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get Linear API key with fallback chain:
|
|
97
|
+
* 1. Project credentials
|
|
98
|
+
* 2. Global keychain
|
|
99
|
+
* 3. Environment variable
|
|
100
|
+
*/
|
|
101
|
+
export async function getLinearApiKey(projectId: string): Promise<string | null> {
|
|
102
|
+
// 1. Project credentials
|
|
103
|
+
const projectCreds = await getProjectCredentials(projectId)
|
|
104
|
+
if (projectCreds.linear?.apiKey) {
|
|
105
|
+
return projectCreds.linear.apiKey
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 2. Global keychain (falls back to env var internally)
|
|
109
|
+
return getCredential('linear-api-key' as CredentialKey)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get Linear team ID from project credentials
|
|
114
|
+
*/
|
|
115
|
+
export async function getLinearTeamId(projectId: string): Promise<string | undefined> {
|
|
116
|
+
const projectCreds = await getProjectCredentials(projectId)
|
|
117
|
+
return projectCreds.linear?.teamId
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if Linear is configured for a project
|
|
122
|
+
*/
|
|
123
|
+
export async function isLinearConfigured(projectId: string): Promise<boolean> {
|
|
124
|
+
const apiKey = await getLinearApiKey(projectId)
|
|
125
|
+
return apiKey !== null && apiKey.length > 0
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get credential source for debugging
|
|
130
|
+
*/
|
|
131
|
+
export async function getCredentialSource(
|
|
132
|
+
projectId: string
|
|
133
|
+
): Promise<'project' | 'keychain' | 'env' | 'none'> {
|
|
134
|
+
// Check project credentials
|
|
135
|
+
const projectCreds = await getProjectCredentials(projectId)
|
|
136
|
+
if (projectCreds.linear?.apiKey) {
|
|
137
|
+
return 'project'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check keychain/env
|
|
141
|
+
const { getCredentialWithSource } = await import('./keychain')
|
|
142
|
+
const result = await getCredentialWithSource('linear-api-key' as CredentialKey)
|
|
143
|
+
if (result.value) {
|
|
144
|
+
return result.source === 'keychain' ? 'keychain' : 'env'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return 'none'
|
|
148
|
+
}
|
package/dist/bin/prjct.mjs
CHANGED
|
@@ -15888,7 +15888,7 @@ var require_package = __commonJS({
|
|
|
15888
15888
|
"package.json"(exports, module) {
|
|
15889
15889
|
module.exports = {
|
|
15890
15890
|
name: "prjct-cli",
|
|
15891
|
-
version: "0.
|
|
15891
|
+
version: "0.41.0",
|
|
15892
15892
|
description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
15893
15893
|
main: "core/index.ts",
|
|
15894
15894
|
bin: {
|
|
@@ -17033,6 +17033,24 @@ if (args[0] === "start" || args[0] === "setup") {
|
|
|
17033
17033
|
console.error("Server error:", error.message);
|
|
17034
17034
|
process.exitCode = 1;
|
|
17035
17035
|
}
|
|
17036
|
+
} else if (args[0] === "linear") {
|
|
17037
|
+
const { spawn } = await import("child_process");
|
|
17038
|
+
const projectPath = process.cwd();
|
|
17039
|
+
const projectId = await config_manager_default.getProjectId(projectPath);
|
|
17040
|
+
if (!projectId) {
|
|
17041
|
+
console.error('No prjct project found. Run "prjct init" first.');
|
|
17042
|
+
process.exitCode = 1;
|
|
17043
|
+
} else {
|
|
17044
|
+
const linearCliPath = path40.join(__dirname, "..", "core", "cli", "linear.ts");
|
|
17045
|
+
const linearArgs = ["--project", projectId, ...args.slice(1)];
|
|
17046
|
+
const child = spawn("bun", [linearCliPath, ...linearArgs], {
|
|
17047
|
+
stdio: "inherit",
|
|
17048
|
+
cwd: projectPath
|
|
17049
|
+
});
|
|
17050
|
+
child.on("close", (code) => {
|
|
17051
|
+
process.exitCode = code || 0;
|
|
17052
|
+
});
|
|
17053
|
+
}
|
|
17036
17054
|
} else if (args[0] === "version" || args[0] === "-v" || args[0] === "--version") {
|
|
17037
17055
|
const detection = detectAllProviders();
|
|
17038
17056
|
const home = os13.homedir();
|