@andypai/agent-kanban 0.1.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/LICENSE +21 -0
- package/README.md +306 -0
- package/package.json +80 -0
- package/src/__tests__/activity.test.ts +139 -0
- package/src/__tests__/api.test.ts +74 -0
- package/src/__tests__/commands/board.test.ts +51 -0
- package/src/__tests__/commands/bulk.test.ts +51 -0
- package/src/__tests__/commands/column.test.ts +78 -0
- package/src/__tests__/commands/task.test.ts +144 -0
- package/src/__tests__/db.test.ts +327 -0
- package/src/__tests__/id.test.ts +19 -0
- package/src/__tests__/index.test.ts +75 -0
- package/src/__tests__/metrics.test.ts +64 -0
- package/src/__tests__/output.test.ts +39 -0
- package/src/activity.ts +73 -0
- package/src/api.ts +209 -0
- package/src/commands/board.ts +29 -0
- package/src/commands/bulk.ts +19 -0
- package/src/commands/column.ts +60 -0
- package/src/commands/task.ts +117 -0
- package/src/config.ts +29 -0
- package/src/db.ts +587 -0
- package/src/errors.ts +32 -0
- package/src/fixtures.ts +128 -0
- package/src/id.ts +8 -0
- package/src/index.ts +413 -0
- package/src/metrics.ts +98 -0
- package/src/output.ts +105 -0
- package/src/providers/capabilities.ts +25 -0
- package/src/providers/errors.ts +16 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/linear-cache.ts +385 -0
- package/src/providers/linear-client.ts +329 -0
- package/src/providers/linear.ts +305 -0
- package/src/providers/local.ts +135 -0
- package/src/providers/types.ts +65 -0
- package/src/server.ts +91 -0
- package/src/types.ts +123 -0
- package/ui/dist/assets/index-DEnUD0fq.css +1 -0
- package/ui/dist/assets/index-DMRjw1nI.js +40 -0
- package/ui/dist/index.html +13 -0
package/src/output.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { CliOutput, BoardView, Task, TaskWithColumn, Column } from './types.ts'
|
|
2
|
+
|
|
3
|
+
export function success<T>(data: T): CliOutput<T> {
|
|
4
|
+
return { ok: true, data }
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function error(code: string, message: string): CliOutput<never> {
|
|
8
|
+
return { ok: false, error: { code, message } }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatOutput(result: CliOutput, pretty: boolean): string {
|
|
12
|
+
if (!pretty) return JSON.stringify(result)
|
|
13
|
+
if (!result.ok) return formatError(result.error)
|
|
14
|
+
return formatPrettyData(result.data)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatError(err: { code: string; message: string }): string {
|
|
18
|
+
return `Error [${err.code}]: ${err.message}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatPrettyData(data: unknown): string {
|
|
22
|
+
if (data && typeof data === 'object' && 'columns' in data) {
|
|
23
|
+
return formatBoard(data as BoardView)
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(data)) {
|
|
26
|
+
if (data.length === 0) return 'No items found.'
|
|
27
|
+
if ('column_id' in data[0]) return data.map(formatTaskLine).join('\n')
|
|
28
|
+
if ('position' in data[0]) return data.map(formatColumnLine).join('\n')
|
|
29
|
+
return JSON.stringify(data, null, 2)
|
|
30
|
+
}
|
|
31
|
+
if (data && typeof data === 'object' && 'column_id' in data) {
|
|
32
|
+
return formatTaskDetail(data as TaskWithColumn)
|
|
33
|
+
}
|
|
34
|
+
if (data && typeof data === 'object' && 'moved' in data) {
|
|
35
|
+
return `Moved ${(data as { moved: number }).moved} task(s).`
|
|
36
|
+
}
|
|
37
|
+
if (data && typeof data === 'object' && 'deleted' in data) {
|
|
38
|
+
return `Deleted ${(data as { deleted: number }).deleted} task(s).`
|
|
39
|
+
}
|
|
40
|
+
if (data && typeof data === 'object' && 'position' in data && 'name' in data) {
|
|
41
|
+
return formatColumnLine(data as Column)
|
|
42
|
+
}
|
|
43
|
+
if (data && typeof data === 'object' && 'message' in data) {
|
|
44
|
+
return (data as { message: string }).message
|
|
45
|
+
}
|
|
46
|
+
return JSON.stringify(data, null, 2)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const PRIORITY_ICONS: Record<string, string> = {
|
|
50
|
+
urgent: '!!!',
|
|
51
|
+
high: '!! ',
|
|
52
|
+
medium: '! ',
|
|
53
|
+
low: '. ',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatTaskLine(task: Task): string {
|
|
57
|
+
const pri = PRIORITY_ICONS[task.priority] ?? ' '
|
|
58
|
+
const assignee = task.assignee ? ` @${task.assignee}` : ''
|
|
59
|
+
const project = task.project ? ` [${task.project}]` : ''
|
|
60
|
+
const ref = task.externalRef && task.externalRef !== task.id ? ` (${task.externalRef})` : ''
|
|
61
|
+
return ` [${pri}] ${task.id}${ref} ${task.title}${assignee}${project}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatTaskDetail(task: Task): string {
|
|
65
|
+
const lines = [
|
|
66
|
+
`Task: ${task.id}`,
|
|
67
|
+
...(task.externalRef && task.externalRef !== task.id ? [`Ref: ${task.externalRef}`] : []),
|
|
68
|
+
`Title: ${task.title}`,
|
|
69
|
+
`Priority: ${task.priority}`,
|
|
70
|
+
]
|
|
71
|
+
if ('column_name' in task && task.column_name) lines.push(`Column: ${task.column_name}`)
|
|
72
|
+
if (task.assignee) lines.push(`Assignee: ${task.assignee}`)
|
|
73
|
+
if (task.project) lines.push(`Project: ${task.project}`)
|
|
74
|
+
if (task.description) lines.push(`Description: ${task.description}`)
|
|
75
|
+
if (task.metadata !== '{}') lines.push(`Metadata: ${task.metadata}`)
|
|
76
|
+
if (task.url) lines.push(`URL: ${task.url}`)
|
|
77
|
+
lines.push(`Created: ${task.created_at}`)
|
|
78
|
+
lines.push(`Updated: ${task.updated_at}`)
|
|
79
|
+
return lines.join('\n')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatColumnLine(col: Column): string {
|
|
83
|
+
const color = col.color ? ` (${col.color})` : ''
|
|
84
|
+
return ` ${col.position}. ${col.name}${color} [${col.id}]`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatBoard(board: BoardView): string {
|
|
88
|
+
const lines: string[] = []
|
|
89
|
+
for (const col of board.columns) {
|
|
90
|
+
const count = col.tasks.length
|
|
91
|
+
lines.push(`── ${col.name} (${count}) ──`)
|
|
92
|
+
if (count === 0) {
|
|
93
|
+
lines.push(' (empty)')
|
|
94
|
+
} else {
|
|
95
|
+
for (const task of col.tasks) {
|
|
96
|
+
const pri = PRIORITY_ICONS[task.priority] ?? ' '
|
|
97
|
+
const assignee = task.assignee ? ` @${task.assignee}` : ''
|
|
98
|
+
const project = task.project ? ` [${task.project}]` : ''
|
|
99
|
+
lines.push(` [${pri}] ${task.id} ${task.title}${assignee}${project}`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
lines.push('')
|
|
103
|
+
}
|
|
104
|
+
return lines.join('\n').trimEnd()
|
|
105
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProviderCapabilities } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
export const LOCAL_CAPABILITIES: ProviderCapabilities = {
|
|
4
|
+
taskCreate: true,
|
|
5
|
+
taskUpdate: true,
|
|
6
|
+
taskMove: true,
|
|
7
|
+
taskDelete: true,
|
|
8
|
+
activity: true,
|
|
9
|
+
metrics: true,
|
|
10
|
+
columnCrud: true,
|
|
11
|
+
bulk: true,
|
|
12
|
+
configEdit: true,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const LINEAR_CAPABILITIES: ProviderCapabilities = {
|
|
16
|
+
taskCreate: true,
|
|
17
|
+
taskUpdate: true,
|
|
18
|
+
taskMove: true,
|
|
19
|
+
taskDelete: false,
|
|
20
|
+
activity: false,
|
|
21
|
+
metrics: false,
|
|
22
|
+
columnCrud: false,
|
|
23
|
+
bulk: false,
|
|
24
|
+
configEdit: false,
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ErrorCode, type ErrorCodeValue, KanbanError } from '../errors.ts'
|
|
2
|
+
|
|
3
|
+
export function unsupportedOperation(message: string): never {
|
|
4
|
+
throw new KanbanError(ErrorCode.UNSUPPORTED_OPERATION, message)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function providerNotConfigured(message: string): never {
|
|
8
|
+
throw new KanbanError(ErrorCode.PROVIDER_NOT_CONFIGURED, message)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function providerUpstreamError(
|
|
12
|
+
message: string,
|
|
13
|
+
code: ErrorCodeValue = ErrorCode.PROVIDER_UPSTREAM_ERROR,
|
|
14
|
+
): never {
|
|
15
|
+
throw new KanbanError(code, message)
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite'
|
|
2
|
+
import { getDbPath, initSchema, seedDefaultColumns } from '../db.ts'
|
|
3
|
+
import { providerNotConfigured } from './errors.ts'
|
|
4
|
+
import { LinearProvider } from './linear.ts'
|
|
5
|
+
import { LocalProvider } from './local.ts'
|
|
6
|
+
import type { KanbanProvider } from './types.ts'
|
|
7
|
+
|
|
8
|
+
export function createProvider(db: Database, dbPath = getDbPath()): KanbanProvider {
|
|
9
|
+
const providerType = (process.env['KANBAN_PROVIDER'] ?? 'local') as 'local' | 'linear'
|
|
10
|
+
if (providerType === 'linear') {
|
|
11
|
+
const apiKey = process.env['LINEAR_API_KEY']
|
|
12
|
+
const teamId = process.env['LINEAR_TEAM_ID']
|
|
13
|
+
if (!apiKey || !teamId) {
|
|
14
|
+
providerNotConfigured(
|
|
15
|
+
'LINEAR_API_KEY and LINEAR_TEAM_ID are required when KANBAN_PROVIDER=linear',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
return new LinearProvider(db, teamId!, apiKey!)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
initSchema(db)
|
|
22
|
+
seedDefaultColumns(db)
|
|
23
|
+
return new LocalProvider(db, dbPath)
|
|
24
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite'
|
|
2
|
+
import type { BoardConfig, BoardView, ProviderTeamInfo, Task } from '../types.ts'
|
|
3
|
+
|
|
4
|
+
export interface LinearStateRow {
|
|
5
|
+
id: string
|
|
6
|
+
name: string
|
|
7
|
+
position: number
|
|
8
|
+
color: string | null
|
|
9
|
+
type: string | null
|
|
10
|
+
created_at: string
|
|
11
|
+
updated_at: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LinearSyncMeta {
|
|
15
|
+
team: ProviderTeamInfo | null
|
|
16
|
+
lastSyncAt: string | null
|
|
17
|
+
lastIssueUpdatedAt: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function initLinearCacheSchema(db: Database): void {
|
|
21
|
+
db.run(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS linear_sync_meta (
|
|
23
|
+
key TEXT PRIMARY KEY,
|
|
24
|
+
value TEXT NOT NULL
|
|
25
|
+
)
|
|
26
|
+
`)
|
|
27
|
+
db.run(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS linear_states (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
name TEXT NOT NULL,
|
|
31
|
+
position INTEGER NOT NULL,
|
|
32
|
+
color TEXT,
|
|
33
|
+
type TEXT,
|
|
34
|
+
created_at TEXT NOT NULL,
|
|
35
|
+
updated_at TEXT NOT NULL
|
|
36
|
+
)
|
|
37
|
+
`)
|
|
38
|
+
db.run(`
|
|
39
|
+
CREATE TABLE IF NOT EXISTS linear_users (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
43
|
+
updated_at TEXT NOT NULL
|
|
44
|
+
)
|
|
45
|
+
`)
|
|
46
|
+
db.run(`
|
|
47
|
+
CREATE TABLE IF NOT EXISTS linear_projects (
|
|
48
|
+
id TEXT PRIMARY KEY,
|
|
49
|
+
name TEXT NOT NULL,
|
|
50
|
+
url TEXT,
|
|
51
|
+
state TEXT,
|
|
52
|
+
updated_at TEXT NOT NULL
|
|
53
|
+
)
|
|
54
|
+
`)
|
|
55
|
+
db.run(`
|
|
56
|
+
CREATE TABLE IF NOT EXISTS linear_issues (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
identifier TEXT NOT NULL UNIQUE,
|
|
59
|
+
title TEXT NOT NULL,
|
|
60
|
+
description TEXT NOT NULL DEFAULT '',
|
|
61
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
62
|
+
assignee_id TEXT,
|
|
63
|
+
assignee_name TEXT NOT NULL DEFAULT '',
|
|
64
|
+
project_id TEXT,
|
|
65
|
+
project_name TEXT NOT NULL DEFAULT '',
|
|
66
|
+
state_id TEXT NOT NULL,
|
|
67
|
+
state_name TEXT NOT NULL,
|
|
68
|
+
state_position INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
url TEXT,
|
|
70
|
+
created_at TEXT NOT NULL,
|
|
71
|
+
updated_at TEXT NOT NULL
|
|
72
|
+
)
|
|
73
|
+
`)
|
|
74
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_linear_issues_state_id ON linear_issues(state_id)')
|
|
75
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_linear_issues_updated_at ON linear_issues(updated_at)')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function setMeta(db: Database, key: string, value: string): void {
|
|
79
|
+
db.query(
|
|
80
|
+
`INSERT INTO linear_sync_meta (key, value) VALUES ($key, $value)
|
|
81
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
82
|
+
).run({ $key: key, $value: value })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getMeta(db: Database, key: string): string | null {
|
|
86
|
+
const row = db.query('SELECT value FROM linear_sync_meta WHERE key = $key').get({
|
|
87
|
+
$key: key,
|
|
88
|
+
}) as { value: string } | null
|
|
89
|
+
return row?.value ?? null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function saveSyncMeta(db: Database, meta: LinearSyncMeta): void {
|
|
93
|
+
if (meta.team) setMeta(db, 'team', JSON.stringify(meta.team))
|
|
94
|
+
if (meta.lastSyncAt) setMeta(db, 'lastSyncAt', meta.lastSyncAt)
|
|
95
|
+
if (meta.lastIssueUpdatedAt) setMeta(db, 'lastIssueUpdatedAt', meta.lastIssueUpdatedAt)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function loadSyncMeta(db: Database): LinearSyncMeta {
|
|
99
|
+
const teamRaw = getMeta(db, 'team')
|
|
100
|
+
return {
|
|
101
|
+
team: teamRaw ? (JSON.parse(teamRaw) as ProviderTeamInfo) : null,
|
|
102
|
+
lastSyncAt: getMeta(db, 'lastSyncAt'),
|
|
103
|
+
lastIssueUpdatedAt: getMeta(db, 'lastIssueUpdatedAt'),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function replaceStates(
|
|
108
|
+
db: Database,
|
|
109
|
+
states: Array<{
|
|
110
|
+
id: string
|
|
111
|
+
name: string
|
|
112
|
+
position: number
|
|
113
|
+
color?: string | null
|
|
114
|
+
type?: string | null
|
|
115
|
+
}>,
|
|
116
|
+
): void {
|
|
117
|
+
const run = db.transaction(() => {
|
|
118
|
+
db.run('DELETE FROM linear_states')
|
|
119
|
+
const stmt = db.prepare(
|
|
120
|
+
`INSERT INTO linear_states (id, name, position, color, type, created_at, updated_at)
|
|
121
|
+
VALUES ($id, $name, $position, $color, $type, datetime('now'), datetime('now'))`,
|
|
122
|
+
)
|
|
123
|
+
for (const state of states) {
|
|
124
|
+
stmt.run({
|
|
125
|
+
$id: state.id,
|
|
126
|
+
$name: state.name,
|
|
127
|
+
$position: state.position,
|
|
128
|
+
$color: state.color ?? null,
|
|
129
|
+
$type: state.type ?? null,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
run()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function upsertUsers(
|
|
137
|
+
db: Database,
|
|
138
|
+
users: Array<{ id: string; name: string; active?: boolean }>,
|
|
139
|
+
): void {
|
|
140
|
+
const stmt = db.prepare(
|
|
141
|
+
`INSERT INTO linear_users (id, name, active, updated_at)
|
|
142
|
+
VALUES ($id, $name, $active, datetime('now'))
|
|
143
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
144
|
+
name = excluded.name,
|
|
145
|
+
active = excluded.active,
|
|
146
|
+
updated_at = excluded.updated_at`,
|
|
147
|
+
)
|
|
148
|
+
for (const user of users) {
|
|
149
|
+
stmt.run({ $id: user.id, $name: user.name, $active: user.active === false ? 0 : 1 })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function upsertProjects(
|
|
154
|
+
db: Database,
|
|
155
|
+
projects: Array<{ id: string; name: string; url?: string | null; state?: string | null }>,
|
|
156
|
+
): void {
|
|
157
|
+
const stmt = db.prepare(
|
|
158
|
+
`INSERT INTO linear_projects (id, name, url, state, updated_at)
|
|
159
|
+
VALUES ($id, $name, $url, $state, datetime('now'))
|
|
160
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
161
|
+
name = excluded.name,
|
|
162
|
+
url = excluded.url,
|
|
163
|
+
state = excluded.state,
|
|
164
|
+
updated_at = excluded.updated_at`,
|
|
165
|
+
)
|
|
166
|
+
for (const project of projects) {
|
|
167
|
+
stmt.run({
|
|
168
|
+
$id: project.id,
|
|
169
|
+
$name: project.name,
|
|
170
|
+
$url: project.url ?? null,
|
|
171
|
+
$state: project.state ?? null,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function upsertIssues(
|
|
177
|
+
db: Database,
|
|
178
|
+
issues: Array<{
|
|
179
|
+
id: string
|
|
180
|
+
identifier: string
|
|
181
|
+
title: string
|
|
182
|
+
description?: string | null
|
|
183
|
+
priority?: number | null
|
|
184
|
+
assigneeId?: string | null
|
|
185
|
+
assigneeName?: string | null
|
|
186
|
+
projectId?: string | null
|
|
187
|
+
projectName?: string | null
|
|
188
|
+
stateId: string
|
|
189
|
+
stateName: string
|
|
190
|
+
statePosition: number
|
|
191
|
+
url?: string | null
|
|
192
|
+
createdAt: string
|
|
193
|
+
updatedAt: string
|
|
194
|
+
}>,
|
|
195
|
+
): void {
|
|
196
|
+
const stmt = db.prepare(
|
|
197
|
+
`INSERT INTO linear_issues (
|
|
198
|
+
id, identifier, title, description, priority, assignee_id, assignee_name,
|
|
199
|
+
project_id, project_name, state_id, state_name, state_position, url, created_at, updated_at
|
|
200
|
+
) VALUES (
|
|
201
|
+
$id, $identifier, $title, $description, $priority, $assignee_id, $assignee_name,
|
|
202
|
+
$project_id, $project_name, $state_id, $state_name, $state_position, $url, $created_at, $updated_at
|
|
203
|
+
)
|
|
204
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
205
|
+
identifier = excluded.identifier,
|
|
206
|
+
title = excluded.title,
|
|
207
|
+
description = excluded.description,
|
|
208
|
+
priority = excluded.priority,
|
|
209
|
+
assignee_id = excluded.assignee_id,
|
|
210
|
+
assignee_name = excluded.assignee_name,
|
|
211
|
+
project_id = excluded.project_id,
|
|
212
|
+
project_name = excluded.project_name,
|
|
213
|
+
state_id = excluded.state_id,
|
|
214
|
+
state_name = excluded.state_name,
|
|
215
|
+
state_position = excluded.state_position,
|
|
216
|
+
url = excluded.url,
|
|
217
|
+
created_at = excluded.created_at,
|
|
218
|
+
updated_at = excluded.updated_at`,
|
|
219
|
+
)
|
|
220
|
+
for (const issue of issues) {
|
|
221
|
+
stmt.run({
|
|
222
|
+
$id: issue.id,
|
|
223
|
+
$identifier: issue.identifier,
|
|
224
|
+
$title: issue.title,
|
|
225
|
+
$description: issue.description ?? '',
|
|
226
|
+
$priority: issue.priority ?? 0,
|
|
227
|
+
$assignee_id: issue.assigneeId ?? null,
|
|
228
|
+
$assignee_name: issue.assigneeName ?? '',
|
|
229
|
+
$project_id: issue.projectId ?? null,
|
|
230
|
+
$project_name: issue.projectName ?? '',
|
|
231
|
+
$state_id: issue.stateId,
|
|
232
|
+
$state_name: issue.stateName,
|
|
233
|
+
$state_position: issue.statePosition,
|
|
234
|
+
$url: issue.url ?? null,
|
|
235
|
+
$created_at: issue.createdAt,
|
|
236
|
+
$updated_at: issue.updatedAt,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getCachedColumns(db: Database): LinearStateRow[] {
|
|
242
|
+
return db.query('SELECT * FROM linear_states ORDER BY position, name').all() as LinearStateRow[]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function mapPriority(priority: number): Task['priority'] {
|
|
246
|
+
switch (priority) {
|
|
247
|
+
case 1:
|
|
248
|
+
return 'urgent'
|
|
249
|
+
case 2:
|
|
250
|
+
return 'high'
|
|
251
|
+
case 3:
|
|
252
|
+
return 'medium'
|
|
253
|
+
case 0:
|
|
254
|
+
case 4:
|
|
255
|
+
default:
|
|
256
|
+
return 'low'
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function taskFromRow(row: {
|
|
261
|
+
id: string
|
|
262
|
+
identifier: string
|
|
263
|
+
title: string
|
|
264
|
+
description: string
|
|
265
|
+
state_id: string
|
|
266
|
+
state_position: number
|
|
267
|
+
priority: number
|
|
268
|
+
assignee_name: string
|
|
269
|
+
project_name: string
|
|
270
|
+
url: string | null
|
|
271
|
+
created_at: string
|
|
272
|
+
updated_at: string
|
|
273
|
+
}): Task {
|
|
274
|
+
return {
|
|
275
|
+
id: `linear:${row.id}`,
|
|
276
|
+
providerId: row.id,
|
|
277
|
+
externalRef: row.identifier,
|
|
278
|
+
url: row.url,
|
|
279
|
+
title: row.title,
|
|
280
|
+
description: row.description,
|
|
281
|
+
column_id: row.state_id,
|
|
282
|
+
position: row.state_position,
|
|
283
|
+
priority: mapPriority(row.priority),
|
|
284
|
+
assignee: row.assignee_name,
|
|
285
|
+
project: row.project_name,
|
|
286
|
+
metadata: '{}',
|
|
287
|
+
created_at: row.created_at,
|
|
288
|
+
updated_at: row.updated_at,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function getCachedBoard(db: Database): BoardView {
|
|
293
|
+
const columns = getCachedColumns(db)
|
|
294
|
+
return {
|
|
295
|
+
columns: columns.map((column) => ({
|
|
296
|
+
...column,
|
|
297
|
+
tasks: (
|
|
298
|
+
db
|
|
299
|
+
.query(
|
|
300
|
+
`SELECT * FROM linear_issues
|
|
301
|
+
WHERE state_id = $state_id
|
|
302
|
+
ORDER BY updated_at DESC, title ASC`,
|
|
303
|
+
)
|
|
304
|
+
.all({ $state_id: column.id }) as Array<{
|
|
305
|
+
id: string
|
|
306
|
+
identifier: string
|
|
307
|
+
title: string
|
|
308
|
+
description: string
|
|
309
|
+
state_id: string
|
|
310
|
+
state_position: number
|
|
311
|
+
priority: number
|
|
312
|
+
assignee_name: string
|
|
313
|
+
project_name: string
|
|
314
|
+
url: string | null
|
|
315
|
+
created_at: string
|
|
316
|
+
updated_at: string
|
|
317
|
+
}>
|
|
318
|
+
).map(taskFromRow),
|
|
319
|
+
})),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function getCachedTask(db: Database, lookup: string): Task | null {
|
|
324
|
+
const normalized = lookup.startsWith('linear:') ? lookup.slice('linear:'.length) : lookup
|
|
325
|
+
const row = db
|
|
326
|
+
.query(
|
|
327
|
+
`SELECT * FROM linear_issues
|
|
328
|
+
WHERE id = $lookup OR identifier = $lookup
|
|
329
|
+
LIMIT 1`,
|
|
330
|
+
)
|
|
331
|
+
.get({ $lookup: normalized }) as {
|
|
332
|
+
id: string
|
|
333
|
+
identifier: string
|
|
334
|
+
title: string
|
|
335
|
+
description: string
|
|
336
|
+
state_id: string
|
|
337
|
+
state_position: number
|
|
338
|
+
priority: number
|
|
339
|
+
assignee_name: string
|
|
340
|
+
project_name: string
|
|
341
|
+
url: string | null
|
|
342
|
+
created_at: string
|
|
343
|
+
updated_at: string
|
|
344
|
+
} | null
|
|
345
|
+
return row ? taskFromRow(row) : null
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function getCachedTasks(db: Database): Task[] {
|
|
349
|
+
return (
|
|
350
|
+
db.query('SELECT * FROM linear_issues ORDER BY updated_at DESC, title ASC').all() as Array<{
|
|
351
|
+
id: string
|
|
352
|
+
identifier: string
|
|
353
|
+
title: string
|
|
354
|
+
description: string
|
|
355
|
+
state_id: string
|
|
356
|
+
state_position: number
|
|
357
|
+
priority: number
|
|
358
|
+
assignee_name: string
|
|
359
|
+
project_name: string
|
|
360
|
+
url: string | null
|
|
361
|
+
created_at: string
|
|
362
|
+
updated_at: string
|
|
363
|
+
}>
|
|
364
|
+
).map(taskFromRow)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function getCachedConfig(db: Database): BoardConfig {
|
|
368
|
+
const members = (
|
|
369
|
+
db
|
|
370
|
+
.query("SELECT name FROM linear_users WHERE active = 1 AND name != '' ORDER BY name")
|
|
371
|
+
.all() as { name: string }[]
|
|
372
|
+
).map((row) => ({ name: row.name, role: 'human' as const }))
|
|
373
|
+
const projects = (
|
|
374
|
+
db.query("SELECT name FROM linear_projects WHERE name != '' ORDER BY name").all() as {
|
|
375
|
+
name: string
|
|
376
|
+
}[]
|
|
377
|
+
).map((row) => row.name)
|
|
378
|
+
return {
|
|
379
|
+
members,
|
|
380
|
+
projects,
|
|
381
|
+
provider: 'linear',
|
|
382
|
+
discoveredAssignees: members.map((member) => member.name),
|
|
383
|
+
discoveredProjects: projects,
|
|
384
|
+
}
|
|
385
|
+
}
|