@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.
Files changed (61) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +2 -1
  2. package/claude-plugin/hooks/hooks.json +41 -0
  3. package/claude-plugin/hooks/session-status.sh +13 -0
  4. package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
  5. package/dist/app.js +228 -28
  6. package/dist/components/CritterHeader.d.ts +7 -0
  7. package/dist/components/CritterHeader.js +81 -0
  8. package/dist/components/HelpOverlay.js +4 -2
  9. package/dist/components/List.d.ts +10 -1
  10. package/dist/components/List.js +14 -5
  11. package/dist/components/Panel.js +27 -1
  12. package/dist/components/SplashScreen.js +1 -1
  13. package/dist/hooks/useSessions.js +2 -2
  14. package/dist/index.js +41 -1
  15. package/dist/lib/auth/index.d.ts +2 -0
  16. package/dist/lib/auth/index.js +3 -0
  17. package/dist/lib/auth/linear.d.ts +20 -0
  18. package/dist/lib/auth/linear.js +79 -0
  19. package/dist/lib/auth/storage.d.ts +12 -0
  20. package/dist/lib/auth/storage.js +53 -0
  21. package/dist/lib/config.d.ts +13 -1
  22. package/dist/lib/config.js +51 -0
  23. package/dist/lib/context.d.ts +10 -0
  24. package/dist/lib/context.js +63 -0
  25. package/dist/lib/critters.d.ts +61 -0
  26. package/dist/lib/critters.js +365 -0
  27. package/dist/lib/hooks.d.ts +20 -0
  28. package/dist/lib/hooks.js +91 -0
  29. package/dist/lib/hotkeys.d.ts +1 -1
  30. package/dist/lib/hotkeys.js +28 -20
  31. package/dist/lib/issues/github.d.ts +11 -0
  32. package/dist/lib/issues/github.js +154 -0
  33. package/dist/lib/issues/index.d.ts +14 -0
  34. package/dist/lib/issues/index.js +27 -0
  35. package/dist/lib/issues/linear.d.ts +24 -0
  36. package/dist/lib/issues/linear.js +345 -0
  37. package/dist/lib/issues/types.d.ts +82 -0
  38. package/dist/lib/issues/types.js +2 -0
  39. package/dist/lib/paths.d.ts +3 -0
  40. package/dist/lib/paths.js +3 -0
  41. package/dist/lib/signals.d.ts +30 -0
  42. package/dist/lib/signals.js +104 -0
  43. package/dist/lib/tmux.d.ts +9 -2
  44. package/dist/lib/tmux.js +114 -18
  45. package/dist/mcp-server.js +161 -1
  46. package/dist/types.d.ts +23 -2
  47. package/dist/views/BarnContext.d.ts +5 -2
  48. package/dist/views/BarnContext.js +202 -21
  49. package/dist/views/CritterDetailView.d.ts +10 -0
  50. package/dist/views/CritterDetailView.js +117 -0
  51. package/dist/views/CritterLogsView.d.ts +8 -0
  52. package/dist/views/CritterLogsView.js +100 -0
  53. package/dist/views/GlobalDashboard.d.ts +2 -2
  54. package/dist/views/GlobalDashboard.js +20 -18
  55. package/dist/views/IssuesView.d.ts +2 -1
  56. package/dist/views/IssuesView.js +661 -98
  57. package/dist/views/LivestockDetailView.d.ts +2 -1
  58. package/dist/views/LivestockDetailView.js +19 -8
  59. package/dist/views/ProjectContext.d.ts +2 -2
  60. package/dist/views/ProjectContext.js +68 -25
  61. 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
+ }
@@ -0,0 +1,2 @@
1
+ // src/lib/issues/types.ts
2
+ export {};
@@ -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.