@colmbus72/yeehaw 0.4.2 → 0.6.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/claude-plugin/.claude-plugin/plugin.json +2 -1
- package/claude-plugin/hooks/hooks.json +41 -0
- package/claude-plugin/hooks/session-status.sh +13 -0
- package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
- package/dist/app.js +228 -28
- package/dist/components/CritterHeader.d.ts +7 -0
- package/dist/components/CritterHeader.js +81 -0
- package/dist/components/HelpOverlay.js +4 -2
- package/dist/components/List.d.ts +10 -1
- package/dist/components/List.js +14 -5
- package/dist/components/Panel.js +27 -1
- package/dist/components/SplashScreen.js +1 -1
- package/dist/hooks/useSessions.js +2 -2
- package/dist/index.js +41 -1
- package/dist/lib/auth/index.d.ts +2 -0
- package/dist/lib/auth/index.js +3 -0
- package/dist/lib/auth/linear.d.ts +20 -0
- package/dist/lib/auth/linear.js +79 -0
- package/dist/lib/auth/storage.d.ts +12 -0
- package/dist/lib/auth/storage.js +53 -0
- package/dist/lib/config.d.ts +13 -1
- package/dist/lib/config.js +51 -0
- package/dist/lib/context.d.ts +10 -0
- package/dist/lib/context.js +63 -0
- package/dist/lib/critters.d.ts +61 -0
- package/dist/lib/critters.js +365 -0
- package/dist/lib/hooks.d.ts +20 -0
- package/dist/lib/hooks.js +91 -0
- package/dist/lib/hotkeys.d.ts +1 -1
- package/dist/lib/hotkeys.js +28 -20
- package/dist/lib/issues/github.d.ts +11 -0
- package/dist/lib/issues/github.js +154 -0
- package/dist/lib/issues/index.d.ts +14 -0
- package/dist/lib/issues/index.js +27 -0
- package/dist/lib/issues/linear.d.ts +24 -0
- package/dist/lib/issues/linear.js +345 -0
- package/dist/lib/issues/types.d.ts +82 -0
- package/dist/lib/issues/types.js +2 -0
- package/dist/lib/paths.d.ts +3 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/signals.d.ts +30 -0
- package/dist/lib/signals.js +104 -0
- package/dist/lib/tmux.d.ts +9 -2
- package/dist/lib/tmux.js +114 -18
- package/dist/mcp-server.js +161 -1
- package/dist/types.d.ts +23 -2
- package/dist/views/BarnContext.d.ts +5 -2
- package/dist/views/BarnContext.js +202 -21
- package/dist/views/CritterDetailView.d.ts +10 -0
- package/dist/views/CritterDetailView.js +117 -0
- package/dist/views/CritterLogsView.d.ts +8 -0
- package/dist/views/CritterLogsView.js +100 -0
- package/dist/views/GlobalDashboard.d.ts +2 -2
- package/dist/views/GlobalDashboard.js +20 -18
- package/dist/views/IssuesView.d.ts +2 -1
- package/dist/views/IssuesView.js +661 -98
- package/dist/views/LivestockDetailView.d.ts +2 -1
- package/dist/views/LivestockDetailView.js +19 -8
- package/dist/views/ProjectContext.d.ts +2 -2
- package/dist/views/ProjectContext.js +68 -25
- package/package.json +5 -5
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// src/lib/issues/github.ts
|
|
2
|
+
import { execaSync } from 'execa';
|
|
3
|
+
/**
|
|
4
|
+
* Parse a GitHub URL to extract owner and repo.
|
|
5
|
+
*/
|
|
6
|
+
function parseGitHubUrl(url) {
|
|
7
|
+
// HTTPS format
|
|
8
|
+
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.\s]+)/);
|
|
9
|
+
if (httpsMatch) {
|
|
10
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2].replace(/\.git$/, '') };
|
|
11
|
+
}
|
|
12
|
+
// SSH format
|
|
13
|
+
const sshMatch = url.match(/github\.com:([^/]+)\/([^/.\s]+)/);
|
|
14
|
+
if (sshMatch) {
|
|
15
|
+
return { owner: sshMatch[1], repo: sshMatch[2].replace(/\.git$/, '') };
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export class GitHubProvider {
|
|
20
|
+
type = 'github';
|
|
21
|
+
repos = [];
|
|
22
|
+
constructor(livestock) {
|
|
23
|
+
// Extract GitHub repos from local livestock only
|
|
24
|
+
const localLivestock = livestock.filter((l) => !l.barn && l.repo);
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const l of localLivestock) {
|
|
27
|
+
if (!l.repo)
|
|
28
|
+
continue;
|
|
29
|
+
const parsed = parseGitHubUrl(l.repo);
|
|
30
|
+
if (parsed) {
|
|
31
|
+
const key = `${parsed.owner}/${parsed.repo}`;
|
|
32
|
+
if (!seen.has(key)) {
|
|
33
|
+
seen.add(key);
|
|
34
|
+
this.repos.push({
|
|
35
|
+
...parsed,
|
|
36
|
+
livestockName: l.name,
|
|
37
|
+
livestockPath: l.path,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async isAuthenticated() {
|
|
44
|
+
try {
|
|
45
|
+
execaSync('gh', ['auth', 'status']);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async authenticate() {
|
|
53
|
+
// GitHub auth is handled externally via `gh auth login`
|
|
54
|
+
// This is a no-op - the view should display instructions
|
|
55
|
+
throw new Error('Run `gh auth login` in your terminal to authenticate with GitHub');
|
|
56
|
+
}
|
|
57
|
+
async fetchIssues(options = {}) {
|
|
58
|
+
const { state = 'open', limit = 50 } = options;
|
|
59
|
+
if (this.repos.length === 0) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const allIssues = [];
|
|
63
|
+
for (const repo of this.repos) {
|
|
64
|
+
try {
|
|
65
|
+
const result = execaSync('gh', [
|
|
66
|
+
'issue', 'list',
|
|
67
|
+
'--repo', `${repo.owner}/${repo.repo}`,
|
|
68
|
+
'--state', state,
|
|
69
|
+
'--limit', String(limit),
|
|
70
|
+
'--json', 'number,title,state,author,labels,createdAt,updatedAt,body,url,comments',
|
|
71
|
+
]);
|
|
72
|
+
const issues = JSON.parse(result.stdout);
|
|
73
|
+
for (const issue of issues) {
|
|
74
|
+
allIssues.push({
|
|
75
|
+
id: `${repo.owner}/${repo.repo}#${issue.number}`,
|
|
76
|
+
identifier: `#${issue.number}`,
|
|
77
|
+
title: issue.title,
|
|
78
|
+
state: issue.state.toLowerCase(),
|
|
79
|
+
isOpen: issue.state.toLowerCase() === 'open',
|
|
80
|
+
author: issue.author.login,
|
|
81
|
+
body: issue.body || '',
|
|
82
|
+
labels: issue.labels.map((l) => l.name),
|
|
83
|
+
url: issue.url,
|
|
84
|
+
createdAt: issue.createdAt,
|
|
85
|
+
updatedAt: issue.updatedAt,
|
|
86
|
+
comments: issue.comments.map((c) => ({
|
|
87
|
+
id: `${issue.number}-${c.createdAt}`,
|
|
88
|
+
author: c.author.login,
|
|
89
|
+
body: c.body,
|
|
90
|
+
createdAt: c.createdAt,
|
|
91
|
+
})),
|
|
92
|
+
source: {
|
|
93
|
+
type: 'github',
|
|
94
|
+
repo: `${repo.owner}/${repo.repo}`,
|
|
95
|
+
livestockName: repo.livestockName,
|
|
96
|
+
livestockPath: repo.livestockPath,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.error(`[github] Failed to fetch issues for ${repo.owner}/${repo.repo}:`, err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Sort by updated date (most recent first)
|
|
106
|
+
allIssues.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
107
|
+
return allIssues;
|
|
108
|
+
}
|
|
109
|
+
async getIssue(id) {
|
|
110
|
+
// Parse id format: "owner/repo#number"
|
|
111
|
+
const match = id.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
112
|
+
if (!match) {
|
|
113
|
+
throw new Error(`Invalid issue ID format: ${id}`);
|
|
114
|
+
}
|
|
115
|
+
const [, owner, repo, numberStr] = match;
|
|
116
|
+
const issueNumber = parseInt(numberStr, 10);
|
|
117
|
+
// Find the livestock for this repo
|
|
118
|
+
const repoInfo = this.repos.find((r) => r.owner === owner && r.repo === repo);
|
|
119
|
+
if (!repoInfo) {
|
|
120
|
+
throw new Error(`Repo not found in livestock: ${owner}/${repo}`);
|
|
121
|
+
}
|
|
122
|
+
const result = execaSync('gh', [
|
|
123
|
+
'issue', 'view', String(issueNumber),
|
|
124
|
+
'--repo', `${owner}/${repo}`,
|
|
125
|
+
'--json', 'number,title,state,author,labels,createdAt,updatedAt,body,url,comments',
|
|
126
|
+
]);
|
|
127
|
+
const issue = JSON.parse(result.stdout);
|
|
128
|
+
return {
|
|
129
|
+
id,
|
|
130
|
+
identifier: `#${issue.number}`,
|
|
131
|
+
title: issue.title,
|
|
132
|
+
state: issue.state.toLowerCase(),
|
|
133
|
+
isOpen: issue.state.toLowerCase() === 'open',
|
|
134
|
+
author: issue.author.login,
|
|
135
|
+
body: issue.body || '',
|
|
136
|
+
labels: issue.labels.map((l) => l.name),
|
|
137
|
+
url: issue.url,
|
|
138
|
+
createdAt: issue.createdAt,
|
|
139
|
+
updatedAt: issue.updatedAt,
|
|
140
|
+
comments: issue.comments.map((c) => ({
|
|
141
|
+
id: `${issue.number}-${c.createdAt}`,
|
|
142
|
+
author: c.author.login,
|
|
143
|
+
body: c.body,
|
|
144
|
+
createdAt: c.createdAt,
|
|
145
|
+
})),
|
|
146
|
+
source: {
|
|
147
|
+
type: 'github',
|
|
148
|
+
repo: `${owner}/${repo}`,
|
|
149
|
+
livestockName: repoInfo.livestockName,
|
|
150
|
+
livestockPath: repoInfo.livestockPath,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Project } from '../../types.js';
|
|
2
|
+
import type { IssueProvider } from './types.js';
|
|
3
|
+
export * from './types.js';
|
|
4
|
+
export { GitHubProvider } from './github.js';
|
|
5
|
+
export { LinearProvider } from './linear.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get the appropriate issue provider for a project.
|
|
8
|
+
* Returns null if issue tracking is disabled.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getProvider(project: Project): IssueProvider | null;
|
|
11
|
+
/**
|
|
12
|
+
* Check if a project has issue tracking enabled.
|
|
13
|
+
*/
|
|
14
|
+
export declare function hasIssueTracking(project: Project): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { GitHubProvider } from './github.js';
|
|
2
|
+
import { LinearProvider } from './linear.js';
|
|
3
|
+
export * from './types.js';
|
|
4
|
+
export { GitHubProvider } from './github.js';
|
|
5
|
+
export { LinearProvider } from './linear.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get the appropriate issue provider for a project.
|
|
8
|
+
* Returns null if issue tracking is disabled.
|
|
9
|
+
*/
|
|
10
|
+
export function getProvider(project) {
|
|
11
|
+
const config = project.issueProvider ?? { type: 'github' };
|
|
12
|
+
switch (config.type) {
|
|
13
|
+
case 'github':
|
|
14
|
+
return new GitHubProvider(project.livestock ?? []);
|
|
15
|
+
case 'linear':
|
|
16
|
+
return new LinearProvider(config.teamId, config.teamName);
|
|
17
|
+
case 'none':
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if a project has issue tracking enabled.
|
|
23
|
+
*/
|
|
24
|
+
export function hasIssueTracking(project) {
|
|
25
|
+
const config = project.issueProvider ?? { type: 'github' };
|
|
26
|
+
return config.type !== 'none';
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Issue, FetchIssuesOptions, LinearProviderInterface, LinearTeam, LinearCycle, LinearAssignee } from './types.js';
|
|
2
|
+
export declare class LinearProvider implements LinearProviderInterface {
|
|
3
|
+
readonly type: "linear";
|
|
4
|
+
private teamId?;
|
|
5
|
+
private teamName?;
|
|
6
|
+
private cachedUserId?;
|
|
7
|
+
private cachedActiveCycleId?;
|
|
8
|
+
constructor(teamId?: string, teamName?: string);
|
|
9
|
+
isAuthenticated(): Promise<boolean>;
|
|
10
|
+
authenticate(): Promise<void>;
|
|
11
|
+
needsTeamSelection(): boolean;
|
|
12
|
+
fetchTeams(): Promise<LinearTeam[]>;
|
|
13
|
+
setTeamId(teamId: string): void;
|
|
14
|
+
setTeamName(teamName: string): void;
|
|
15
|
+
getTeamName(): string | undefined;
|
|
16
|
+
fetchTeamName(): Promise<string | undefined>;
|
|
17
|
+
getCurrentUserId(): Promise<string | null>;
|
|
18
|
+
fetchCycles(): Promise<LinearCycle[]>;
|
|
19
|
+
getActiveCycleId(): string | undefined;
|
|
20
|
+
fetchAssignees(): Promise<LinearAssignee[]>;
|
|
21
|
+
fetchIssues(options?: FetchIssuesOptions): Promise<Issue[]>;
|
|
22
|
+
getIssue(id: string): Promise<Issue>;
|
|
23
|
+
private normalizeIssue;
|
|
24
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { isLinearAuthenticated, linearGraphQL } from '../auth/index.js';
|
|
2
|
+
// GraphQL queries
|
|
3
|
+
const TEAMS_QUERY = `
|
|
4
|
+
query {
|
|
5
|
+
teams {
|
|
6
|
+
nodes {
|
|
7
|
+
id
|
|
8
|
+
name
|
|
9
|
+
key
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
const VIEWER_QUERY = `
|
|
15
|
+
query {
|
|
16
|
+
viewer {
|
|
17
|
+
id
|
|
18
|
+
name
|
|
19
|
+
displayName
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
const CYCLES_QUERY = `
|
|
24
|
+
query($teamId: String!) {
|
|
25
|
+
team(id: $teamId) {
|
|
26
|
+
cycles(orderBy: startsAt, first: 20) {
|
|
27
|
+
nodes {
|
|
28
|
+
id
|
|
29
|
+
name
|
|
30
|
+
number
|
|
31
|
+
startsAt
|
|
32
|
+
endsAt
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
activeCycle {
|
|
36
|
+
id
|
|
37
|
+
name
|
|
38
|
+
number
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
const ASSIGNEES_QUERY = `
|
|
44
|
+
query($teamId: String!) {
|
|
45
|
+
team(id: $teamId) {
|
|
46
|
+
members {
|
|
47
|
+
nodes {
|
|
48
|
+
id
|
|
49
|
+
name
|
|
50
|
+
displayName
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
const ISSUES_QUERY = `
|
|
57
|
+
query($teamId: String!, $first: Int, $filter: IssueFilter) {
|
|
58
|
+
team(id: $teamId) {
|
|
59
|
+
issues(first: $first, filter: $filter, orderBy: updatedAt) {
|
|
60
|
+
nodes {
|
|
61
|
+
id
|
|
62
|
+
identifier
|
|
63
|
+
title
|
|
64
|
+
description
|
|
65
|
+
url
|
|
66
|
+
createdAt
|
|
67
|
+
updatedAt
|
|
68
|
+
priority
|
|
69
|
+
estimate
|
|
70
|
+
state {
|
|
71
|
+
name
|
|
72
|
+
type
|
|
73
|
+
}
|
|
74
|
+
creator {
|
|
75
|
+
name
|
|
76
|
+
}
|
|
77
|
+
assignee {
|
|
78
|
+
id
|
|
79
|
+
name
|
|
80
|
+
displayName
|
|
81
|
+
}
|
|
82
|
+
cycle {
|
|
83
|
+
id
|
|
84
|
+
name
|
|
85
|
+
number
|
|
86
|
+
}
|
|
87
|
+
labels {
|
|
88
|
+
nodes {
|
|
89
|
+
name
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
comments {
|
|
93
|
+
nodes {
|
|
94
|
+
id
|
|
95
|
+
body
|
|
96
|
+
createdAt
|
|
97
|
+
user {
|
|
98
|
+
name
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
const ISSUE_QUERY = `
|
|
108
|
+
query($id: String!) {
|
|
109
|
+
issue(id: $id) {
|
|
110
|
+
id
|
|
111
|
+
identifier
|
|
112
|
+
title
|
|
113
|
+
description
|
|
114
|
+
url
|
|
115
|
+
createdAt
|
|
116
|
+
updatedAt
|
|
117
|
+
priority
|
|
118
|
+
estimate
|
|
119
|
+
state {
|
|
120
|
+
name
|
|
121
|
+
type
|
|
122
|
+
}
|
|
123
|
+
creator {
|
|
124
|
+
name
|
|
125
|
+
}
|
|
126
|
+
assignee {
|
|
127
|
+
id
|
|
128
|
+
name
|
|
129
|
+
displayName
|
|
130
|
+
}
|
|
131
|
+
cycle {
|
|
132
|
+
id
|
|
133
|
+
name
|
|
134
|
+
number
|
|
135
|
+
}
|
|
136
|
+
labels {
|
|
137
|
+
nodes {
|
|
138
|
+
name
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
comments {
|
|
142
|
+
nodes {
|
|
143
|
+
id
|
|
144
|
+
body
|
|
145
|
+
createdAt
|
|
146
|
+
user {
|
|
147
|
+
name
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
team {
|
|
152
|
+
name
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
export class LinearProvider {
|
|
158
|
+
type = 'linear';
|
|
159
|
+
teamId;
|
|
160
|
+
teamName;
|
|
161
|
+
cachedUserId;
|
|
162
|
+
cachedActiveCycleId;
|
|
163
|
+
constructor(teamId, teamName) {
|
|
164
|
+
this.teamId = teamId;
|
|
165
|
+
this.teamName = teamName;
|
|
166
|
+
}
|
|
167
|
+
async isAuthenticated() {
|
|
168
|
+
return isLinearAuthenticated();
|
|
169
|
+
}
|
|
170
|
+
async authenticate() {
|
|
171
|
+
throw new Error('Use saveLinearApiKey() to authenticate with a Personal API Key');
|
|
172
|
+
}
|
|
173
|
+
needsTeamSelection() {
|
|
174
|
+
return !this.teamId;
|
|
175
|
+
}
|
|
176
|
+
async fetchTeams() {
|
|
177
|
+
const data = await linearGraphQL(TEAMS_QUERY);
|
|
178
|
+
return data.teams.nodes;
|
|
179
|
+
}
|
|
180
|
+
setTeamId(teamId) {
|
|
181
|
+
this.teamId = teamId;
|
|
182
|
+
}
|
|
183
|
+
setTeamName(teamName) {
|
|
184
|
+
this.teamName = teamName;
|
|
185
|
+
}
|
|
186
|
+
getTeamName() {
|
|
187
|
+
return this.teamName;
|
|
188
|
+
}
|
|
189
|
+
async fetchTeamName() {
|
|
190
|
+
if (this.teamName)
|
|
191
|
+
return this.teamName;
|
|
192
|
+
if (!this.teamId)
|
|
193
|
+
return undefined;
|
|
194
|
+
try {
|
|
195
|
+
const data = await linearGraphQL(`query($teamId: String!) { team(id: $teamId) { name } }`, { teamId: this.teamId });
|
|
196
|
+
this.teamName = data.team.name;
|
|
197
|
+
return this.teamName;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async getCurrentUserId() {
|
|
204
|
+
if (this.cachedUserId !== undefined) {
|
|
205
|
+
return this.cachedUserId;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const data = await linearGraphQL(VIEWER_QUERY);
|
|
209
|
+
this.cachedUserId = data.viewer.id;
|
|
210
|
+
return this.cachedUserId;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
this.cachedUserId = null;
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async fetchCycles() {
|
|
218
|
+
if (!this.teamId) {
|
|
219
|
+
throw new Error('Team not selected');
|
|
220
|
+
}
|
|
221
|
+
const data = await linearGraphQL(CYCLES_QUERY, { teamId: this.teamId });
|
|
222
|
+
// Cache the active cycle ID
|
|
223
|
+
if (data.team.activeCycle) {
|
|
224
|
+
this.cachedActiveCycleId = data.team.activeCycle.id;
|
|
225
|
+
}
|
|
226
|
+
return data.team.cycles.nodes.map((c) => ({
|
|
227
|
+
id: c.id,
|
|
228
|
+
name: c.name || `Cycle ${c.number}`,
|
|
229
|
+
number: c.number,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
getActiveCycleId() {
|
|
233
|
+
return this.cachedActiveCycleId;
|
|
234
|
+
}
|
|
235
|
+
async fetchAssignees() {
|
|
236
|
+
if (!this.teamId) {
|
|
237
|
+
throw new Error('Team not selected');
|
|
238
|
+
}
|
|
239
|
+
const data = await linearGraphQL(ASSIGNEES_QUERY, { teamId: this.teamId });
|
|
240
|
+
return data.team.members.nodes;
|
|
241
|
+
}
|
|
242
|
+
async fetchIssues(options = {}) {
|
|
243
|
+
if (!this.teamId) {
|
|
244
|
+
throw new Error('Team not selected');
|
|
245
|
+
}
|
|
246
|
+
const { state = 'open', limit = 50, linearFilter } = options;
|
|
247
|
+
// Build filter object
|
|
248
|
+
const filter = {};
|
|
249
|
+
// State filter
|
|
250
|
+
if (state === 'open') {
|
|
251
|
+
filter.state = { type: { in: ['backlog', 'unstarted', 'started'] } };
|
|
252
|
+
}
|
|
253
|
+
else if (state === 'closed') {
|
|
254
|
+
filter.state = { type: { in: ['completed', 'canceled'] } };
|
|
255
|
+
}
|
|
256
|
+
// Linear-specific filters
|
|
257
|
+
if (linearFilter) {
|
|
258
|
+
// Assignee filter
|
|
259
|
+
if (linearFilter.assigneeId !== undefined) {
|
|
260
|
+
if (linearFilter.assigneeId === null) {
|
|
261
|
+
filter.assignee = { null: true };
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
filter.assignee = { id: { eq: linearFilter.assigneeId } };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Cycle filter
|
|
268
|
+
if (linearFilter.cycleId) {
|
|
269
|
+
filter.cycle = { id: { eq: linearFilter.cycleId } };
|
|
270
|
+
}
|
|
271
|
+
// State type filter (overrides basic state filter)
|
|
272
|
+
if (linearFilter.stateType) {
|
|
273
|
+
const types = Array.isArray(linearFilter.stateType)
|
|
274
|
+
? linearFilter.stateType
|
|
275
|
+
: [linearFilter.stateType];
|
|
276
|
+
filter.state = { type: { in: types } };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const data = await linearGraphQL(ISSUES_QUERY, { teamId: this.teamId, first: limit, filter: Object.keys(filter).length > 0 ? filter : undefined });
|
|
280
|
+
let issues = data.team.issues.nodes.map((issue) => this.normalizeIssue(issue));
|
|
281
|
+
// Client-side sorting
|
|
282
|
+
if (linearFilter?.sortBy === 'priority') {
|
|
283
|
+
// Priority: 1 = urgent (highest), 4 = low, 0 = no priority (lowest)
|
|
284
|
+
issues = issues.sort((a, b) => {
|
|
285
|
+
const aPri = a.priority === 0 ? 5 : (a.priority ?? 5);
|
|
286
|
+
const bPri = b.priority === 0 ? 5 : (b.priority ?? 5);
|
|
287
|
+
return linearFilter.sortDirection === 'desc' ? aPri - bPri : bPri - aPri;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else if (linearFilter?.sortBy === 'createdAt') {
|
|
291
|
+
issues = issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
292
|
+
}
|
|
293
|
+
// Default: already sorted by updatedAt from API
|
|
294
|
+
return issues;
|
|
295
|
+
}
|
|
296
|
+
async getIssue(id) {
|
|
297
|
+
const data = await linearGraphQL(ISSUE_QUERY, { id });
|
|
298
|
+
return this.normalizeIssue(data.issue);
|
|
299
|
+
}
|
|
300
|
+
normalizeIssue(issue) {
|
|
301
|
+
const openStateTypes = ['backlog', 'unstarted', 'started'];
|
|
302
|
+
const isOpen = openStateTypes.includes(issue.state.type);
|
|
303
|
+
const comments = issue.comments.nodes.map((c) => ({
|
|
304
|
+
id: c.id,
|
|
305
|
+
author: c.user?.name || 'Unknown',
|
|
306
|
+
body: c.body,
|
|
307
|
+
createdAt: c.createdAt,
|
|
308
|
+
}));
|
|
309
|
+
return {
|
|
310
|
+
id: issue.id,
|
|
311
|
+
identifier: issue.identifier,
|
|
312
|
+
title: issue.title,
|
|
313
|
+
state: issue.state.name,
|
|
314
|
+
stateType: issue.state.type,
|
|
315
|
+
isOpen,
|
|
316
|
+
author: issue.creator?.name || 'Unknown',
|
|
317
|
+
body: issue.description || '',
|
|
318
|
+
labels: issue.labels.nodes.map((l) => l.name),
|
|
319
|
+
url: issue.url,
|
|
320
|
+
createdAt: issue.createdAt,
|
|
321
|
+
updatedAt: issue.updatedAt,
|
|
322
|
+
comments,
|
|
323
|
+
source: {
|
|
324
|
+
type: 'linear',
|
|
325
|
+
team: issue.team?.name || this.teamName || 'Unknown',
|
|
326
|
+
},
|
|
327
|
+
priority: issue.priority,
|
|
328
|
+
estimate: issue.estimate ?? undefined,
|
|
329
|
+
assignee: issue.assignee
|
|
330
|
+
? {
|
|
331
|
+
id: issue.assignee.id,
|
|
332
|
+
name: issue.assignee.name,
|
|
333
|
+
displayName: issue.assignee.displayName,
|
|
334
|
+
}
|
|
335
|
+
: undefined,
|
|
336
|
+
cycle: issue.cycle
|
|
337
|
+
? {
|
|
338
|
+
id: issue.cycle.id,
|
|
339
|
+
name: issue.cycle.name || `Cycle ${issue.cycle.number}`,
|
|
340
|
+
number: issue.cycle.number,
|
|
341
|
+
}
|
|
342
|
+
: undefined,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export interface IssueComment {
|
|
2
|
+
id: string;
|
|
3
|
+
author: string;
|
|
4
|
+
body: string;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
}
|
|
7
|
+
export type LinearPriority = 0 | 1 | 2 | 3 | 4;
|
|
8
|
+
export type LinearStateType = 'backlog' | 'unstarted' | 'started' | 'completed' | 'canceled' | 'triage';
|
|
9
|
+
export interface LinearAssignee {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LinearCycle {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
number: number;
|
|
18
|
+
}
|
|
19
|
+
export interface Issue {
|
|
20
|
+
id: string;
|
|
21
|
+
identifier: string;
|
|
22
|
+
title: string;
|
|
23
|
+
state: string;
|
|
24
|
+
stateType?: LinearStateType;
|
|
25
|
+
isOpen: boolean;
|
|
26
|
+
author: string;
|
|
27
|
+
body: string;
|
|
28
|
+
labels: string[];
|
|
29
|
+
url: string;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
comments: IssueComment[];
|
|
33
|
+
source: IssueSource;
|
|
34
|
+
priority?: LinearPriority;
|
|
35
|
+
estimate?: number;
|
|
36
|
+
assignee?: LinearAssignee;
|
|
37
|
+
cycle?: LinearCycle;
|
|
38
|
+
}
|
|
39
|
+
export type IssueSource = {
|
|
40
|
+
type: 'github';
|
|
41
|
+
repo: string;
|
|
42
|
+
livestockName: string;
|
|
43
|
+
livestockPath: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'linear';
|
|
46
|
+
team: string;
|
|
47
|
+
};
|
|
48
|
+
export interface LinearIssueFilter {
|
|
49
|
+
assigneeId?: string | null;
|
|
50
|
+
cycleId?: string;
|
|
51
|
+
stateType?: LinearStateType | LinearStateType[];
|
|
52
|
+
sortBy?: 'priority' | 'updatedAt' | 'createdAt';
|
|
53
|
+
sortDirection?: 'asc' | 'desc';
|
|
54
|
+
}
|
|
55
|
+
export interface FetchIssuesOptions {
|
|
56
|
+
state?: 'open' | 'closed' | 'all';
|
|
57
|
+
limit?: number;
|
|
58
|
+
linearFilter?: LinearIssueFilter;
|
|
59
|
+
}
|
|
60
|
+
export interface IssueProvider {
|
|
61
|
+
readonly type: 'github' | 'linear';
|
|
62
|
+
isAuthenticated(): Promise<boolean>;
|
|
63
|
+
authenticate(): Promise<void>;
|
|
64
|
+
fetchIssues(options?: FetchIssuesOptions): Promise<Issue[]>;
|
|
65
|
+
getIssue(id: string): Promise<Issue>;
|
|
66
|
+
}
|
|
67
|
+
export interface LinearProviderInterface extends IssueProvider {
|
|
68
|
+
readonly type: 'linear';
|
|
69
|
+
needsTeamSelection(): boolean;
|
|
70
|
+
fetchTeams(): Promise<LinearTeam[]>;
|
|
71
|
+
setTeamId(teamId: string): void;
|
|
72
|
+
setTeamName(teamName: string): void;
|
|
73
|
+
getTeamName(): string | undefined;
|
|
74
|
+
fetchCycles(): Promise<LinearCycle[]>;
|
|
75
|
+
fetchAssignees(): Promise<LinearAssignee[]>;
|
|
76
|
+
getCurrentUserId(): Promise<string | null>;
|
|
77
|
+
}
|
|
78
|
+
export interface LinearTeam {
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
key: string;
|
|
82
|
+
}
|
package/dist/lib/paths.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export declare const YEEHAW_DIR: string;
|
|
2
2
|
export declare const CONFIG_FILE: string;
|
|
3
|
+
export declare const AUTH_FILE: string;
|
|
3
4
|
export declare const PROJECTS_DIR: string;
|
|
4
5
|
export declare const BARNS_DIR: string;
|
|
5
6
|
export declare const SESSIONS_DIR: string;
|
|
7
|
+
export declare const SIGNALS_DIR: string;
|
|
8
|
+
export declare const HOOKS_DIR: string;
|
|
6
9
|
export declare function getProjectPath(name: string): string;
|
|
7
10
|
export declare function getBarnPath(name: string): string;
|
|
8
11
|
export declare function getSessionPath(id: string): string;
|
package/dist/lib/paths.js
CHANGED
|
@@ -2,9 +2,12 @@ import { homedir } from 'os';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
export const YEEHAW_DIR = join(homedir(), '.yeehaw');
|
|
4
4
|
export const CONFIG_FILE = join(YEEHAW_DIR, 'config.yaml');
|
|
5
|
+
export const AUTH_FILE = join(YEEHAW_DIR, 'auth.yaml');
|
|
5
6
|
export const PROJECTS_DIR = join(YEEHAW_DIR, 'projects');
|
|
6
7
|
export const BARNS_DIR = join(YEEHAW_DIR, 'barns');
|
|
7
8
|
export const SESSIONS_DIR = join(YEEHAW_DIR, 'sessions');
|
|
9
|
+
export const SIGNALS_DIR = join(YEEHAW_DIR, 'session-signals');
|
|
10
|
+
export const HOOKS_DIR = join(YEEHAW_DIR, 'bin');
|
|
8
11
|
/**
|
|
9
12
|
* Validate a name to prevent path traversal attacks.
|
|
10
13
|
* Rejects names containing path separators or parent directory references.
|