@0xbigboss/gh-pulse-core 1.0.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 (73) hide show
  1. package/dist/cache.cjs +312 -0
  2. package/dist/cache.d.cts +58 -0
  3. package/dist/cache.d.ts +58 -0
  4. package/dist/cache.js +303 -0
  5. package/dist/config.cjs +182 -0
  6. package/dist/config.d.cts +248 -0
  7. package/dist/config.d.ts +248 -0
  8. package/dist/config.js +172 -0
  9. package/dist/filters.cjs +16 -0
  10. package/dist/filters.d.cts +3 -0
  11. package/dist/filters.d.ts +3 -0
  12. package/dist/filters.js +12 -0
  13. package/dist/github.cjs +240 -0
  14. package/dist/github.d.cts +46 -0
  15. package/dist/github.d.ts +46 -0
  16. package/dist/github.js +235 -0
  17. package/dist/index.cjs +28 -0
  18. package/dist/index.d.cts +11 -0
  19. package/dist/index.d.ts +11 -0
  20. package/dist/index.js +11 -0
  21. package/dist/reports/context.cjs +8 -0
  22. package/dist/reports/context.d.cts +7 -0
  23. package/dist/reports/context.d.ts +7 -0
  24. package/dist/reports/context.js +5 -0
  25. package/dist/reports/exec.cjs +160 -0
  26. package/dist/reports/exec.d.cts +6 -0
  27. package/dist/reports/exec.d.ts +6 -0
  28. package/dist/reports/exec.js +157 -0
  29. package/dist/reports/index.cjs +21 -0
  30. package/dist/reports/index.d.cts +5 -0
  31. package/dist/reports/index.d.ts +5 -0
  32. package/dist/reports/index.js +5 -0
  33. package/dist/reports/meta.cjs +15 -0
  34. package/dist/reports/meta.d.cts +12 -0
  35. package/dist/reports/meta.d.ts +12 -0
  36. package/dist/reports/meta.js +12 -0
  37. package/dist/reports/personal.cjs +90 -0
  38. package/dist/reports/personal.d.cts +8 -0
  39. package/dist/reports/personal.d.ts +8 -0
  40. package/dist/reports/personal.js +87 -0
  41. package/dist/reports/team.cjs +127 -0
  42. package/dist/reports/team.d.cts +6 -0
  43. package/dist/reports/team.d.ts +6 -0
  44. package/dist/reports/team.js +124 -0
  45. package/dist/reports/types.cjs +2 -0
  46. package/dist/reports/types.d.cts +144 -0
  47. package/dist/reports/types.d.ts +144 -0
  48. package/dist/reports/types.js +1 -0
  49. package/dist/reports/utils.cjs +71 -0
  50. package/dist/reports/utils.d.cts +6 -0
  51. package/dist/reports/utils.d.ts +6 -0
  52. package/dist/reports/utils.js +65 -0
  53. package/dist/repos.cjs +102 -0
  54. package/dist/repos.d.cts +12 -0
  55. package/dist/repos.d.ts +12 -0
  56. package/dist/repos.js +96 -0
  57. package/dist/sync.cjs +360 -0
  58. package/dist/sync.d.cts +24 -0
  59. package/dist/sync.d.ts +24 -0
  60. package/dist/sync.js +357 -0
  61. package/dist/team.cjs +45 -0
  62. package/dist/team.d.cts +10 -0
  63. package/dist/team.d.ts +10 -0
  64. package/dist/team.js +42 -0
  65. package/dist/time.cjs +153 -0
  66. package/dist/time.d.cts +13 -0
  67. package/dist/time.d.ts +13 -0
  68. package/dist/time.js +145 -0
  69. package/dist/types.cjs +2 -0
  70. package/dist/types.d.cts +133 -0
  71. package/dist/types.d.ts +133 -0
  72. package/dist/types.js +1 -0
  73. package/package.json +29 -0
package/dist/cache.cjs ADDED
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Cache = void 0;
7
+ exports.getEventTimestamp = getEventTimestamp;
8
+ exports.toCommitFromEvent = toCommitFromEvent;
9
+ const bun_sqlite_1 = require("bun:sqlite");
10
+ const node_fs_1 = require("node:fs");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ class Cache {
13
+ db;
14
+ now;
15
+ constructor(db, now) {
16
+ this.db = db;
17
+ this.now = now;
18
+ }
19
+ static async open(options) {
20
+ await ensureDir(options.path);
21
+ const db = new bun_sqlite_1.Database(options.path);
22
+ const cache = new Cache(db, options.now ?? (() => Date.now()));
23
+ cache.init();
24
+ return cache;
25
+ }
26
+ init() {
27
+ this.db.exec(`
28
+ CREATE TABLE IF NOT EXISTS meta (
29
+ key TEXT PRIMARY KEY,
30
+ value TEXT NOT NULL
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS events (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ type TEXT NOT NULL,
36
+ repo TEXT NOT NULL,
37
+ payload JSON NOT NULL,
38
+ timestamp INTEGER NOT NULL,
39
+ fetched_at INTEGER NOT NULL
40
+ );
41
+
42
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
43
+ CREATE INDEX IF NOT EXISTS idx_events_repo ON events(repo);
44
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
45
+
46
+ CREATE TABLE IF NOT EXISTS pull_requests (
47
+ repo TEXT NOT NULL,
48
+ number INTEGER NOT NULL,
49
+ data JSON NOT NULL,
50
+ fetched_at INTEGER NOT NULL,
51
+ PRIMARY KEY (repo, number)
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_pr_author ON pull_requests((json_extract(data, '$.author')));
55
+ CREATE INDEX IF NOT EXISTS idx_pr_state ON pull_requests((json_extract(data, '$.state')));
56
+
57
+ CREATE TABLE IF NOT EXISTS commits (
58
+ repo TEXT NOT NULL,
59
+ sha TEXT NOT NULL,
60
+ data JSON NOT NULL,
61
+ fetched_at INTEGER NOT NULL,
62
+ PRIMARY KEY (repo, sha)
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS idx_commits_author ON commits((json_extract(data, '$.author')));
66
+ CREATE INDEX IF NOT EXISTS idx_commits_timestamp ON commits((json_extract(data, '$.committed_at')));
67
+
68
+ CREATE TABLE IF NOT EXISTS linear_issues (
69
+ id TEXT PRIMARY KEY,
70
+ data JSON NOT NULL,
71
+ fetched_at INTEGER NOT NULL
72
+ );
73
+
74
+ CREATE INDEX IF NOT EXISTS idx_linear_state ON linear_issues((json_extract(data, '$.state')));
75
+
76
+ CREATE TABLE IF NOT EXISTS pr_issue_links (
77
+ repo TEXT NOT NULL,
78
+ pr_number INTEGER NOT NULL,
79
+ issue_id TEXT NOT NULL,
80
+ correlation_method TEXT NOT NULL,
81
+ PRIMARY KEY (repo, pr_number, issue_id)
82
+ );
83
+
84
+ CREATE TABLE IF NOT EXISTS sync_state (
85
+ repo TEXT PRIMARY KEY,
86
+ last_sync INTEGER NOT NULL,
87
+ cursor TEXT
88
+ );
89
+ `);
90
+ }
91
+ close() {
92
+ this.db.close();
93
+ }
94
+ insertEvent(event, fetchedAt = this.now()) {
95
+ const timestamp = getEventTimestamp(event);
96
+ if (this.eventExists(event, timestamp)) {
97
+ return false;
98
+ }
99
+ this.db.run('INSERT INTO events (type, repo, payload, timestamp, fetched_at) VALUES (?, ?, ?, ?, ?)', [event.type, event.repo, JSON.stringify(event), timestamp, fetchedAt]);
100
+ return true;
101
+ }
102
+ upsertPullRequest(pr, fetchedAt = this.now()) {
103
+ this.db.run(`INSERT INTO pull_requests (repo, number, data, fetched_at)
104
+ VALUES (?, ?, ?, ?)
105
+ ON CONFLICT(repo, number) DO UPDATE SET data = excluded.data, fetched_at = excluded.fetched_at`, [pr.repo, pr.number, JSON.stringify(pr), fetchedAt]);
106
+ }
107
+ upsertCommit(commit, fetchedAt = this.now()) {
108
+ this.db.run(`INSERT INTO commits (repo, sha, data, fetched_at)
109
+ VALUES (?, ?, ?, ?)
110
+ ON CONFLICT(repo, sha) DO UPDATE SET data = excluded.data, fetched_at = excluded.fetched_at`, [commit.repo, commit.sha, JSON.stringify(commit), fetchedAt]);
111
+ }
112
+ getPullRequest(repo, number) {
113
+ const row = this.db
114
+ .query('SELECT data, fetched_at FROM pull_requests WHERE repo = ? AND number = ?')
115
+ .get(repo, number);
116
+ if (!row) {
117
+ return null;
118
+ }
119
+ return {
120
+ data: JSON.parse(row.data),
121
+ fetched_at: row.fetched_at,
122
+ };
123
+ }
124
+ getPullRequests(filter = {}) {
125
+ const conditions = [];
126
+ const params = [];
127
+ if (filter.repo) {
128
+ conditions.push('repo = ?');
129
+ params.push(filter.repo);
130
+ }
131
+ if (filter.author) {
132
+ conditions.push("json_extract(data, '$.author') = ?");
133
+ params.push(filter.author);
134
+ }
135
+ if (filter.state) {
136
+ conditions.push("json_extract(data, '$.state') = ?");
137
+ params.push(filter.state);
138
+ }
139
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
140
+ const rows = this.db.query(`SELECT data FROM pull_requests ${where}`).all(...params);
141
+ return rows.map((row) => JSON.parse(row.data));
142
+ }
143
+ getCommits(filter = {}) {
144
+ const conditions = [];
145
+ const params = [];
146
+ if (filter.repo) {
147
+ conditions.push('repo = ?');
148
+ params.push(filter.repo);
149
+ }
150
+ if (filter.author) {
151
+ conditions.push("json_extract(data, '$.author') = ?");
152
+ params.push(filter.author);
153
+ }
154
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
155
+ const rows = this.db.query(`SELECT data FROM commits ${where}`).all(...params);
156
+ return rows.map((row) => JSON.parse(row.data));
157
+ }
158
+ getCommit(repo, sha) {
159
+ const row = this.db
160
+ .query('SELECT data, fetched_at FROM commits WHERE repo = ? AND sha = ?')
161
+ .get(repo, sha);
162
+ if (!row) {
163
+ return null;
164
+ }
165
+ return {
166
+ data: JSON.parse(row.data),
167
+ fetched_at: row.fetched_at,
168
+ };
169
+ }
170
+ getEvents(filter = {}) {
171
+ const conditions = [];
172
+ const params = [];
173
+ if (filter.repo) {
174
+ conditions.push('repo = ?');
175
+ params.push(filter.repo);
176
+ }
177
+ if (filter.type) {
178
+ conditions.push('type = ?');
179
+ params.push(filter.type);
180
+ }
181
+ if (filter.since !== undefined) {
182
+ conditions.push('timestamp >= ?');
183
+ params.push(filter.since);
184
+ }
185
+ if (filter.until !== undefined) {
186
+ conditions.push('timestamp <= ?');
187
+ params.push(filter.until);
188
+ }
189
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
190
+ const rows = this.db
191
+ .query(`SELECT payload FROM events ${where} ORDER BY timestamp ASC`)
192
+ .all(...params);
193
+ return rows.map((row) => JSON.parse(row.payload));
194
+ }
195
+ getEventsForPr(repo, prNumber) {
196
+ const rows = this.db
197
+ .query("SELECT payload FROM events WHERE repo = ? AND json_extract(payload, '$.pr_number') = ? ORDER BY timestamp ASC")
198
+ .all(repo, prNumber);
199
+ return rows.map((row) => JSON.parse(row.payload));
200
+ }
201
+ getSyncState(repo) {
202
+ const row = this.db
203
+ .query('SELECT repo, last_sync, cursor FROM sync_state WHERE repo = ?')
204
+ .get(repo);
205
+ if (!row) {
206
+ return null;
207
+ }
208
+ return {
209
+ repo: row.repo,
210
+ last_sync: row.last_sync,
211
+ cursor: row.cursor,
212
+ };
213
+ }
214
+ upsertSyncState(repo, lastSync, cursor = null) {
215
+ this.db.run(`INSERT INTO sync_state (repo, last_sync, cursor)
216
+ VALUES (?, ?, ?)
217
+ ON CONFLICT(repo) DO UPDATE SET last_sync = excluded.last_sync, cursor = excluded.cursor`, [repo, lastSync, cursor]);
218
+ }
219
+ getFreshness(repos) {
220
+ const now = this.now();
221
+ const eventsMax = this.db.query('SELECT MAX(fetched_at) as max FROM events').get();
222
+ const prMax = this.db.query('SELECT MAX(fetched_at) as max FROM pull_requests').get();
223
+ const commitMax = this.db.query('SELECT MAX(fetched_at) as max FROM commits').get();
224
+ const values = [eventsMax?.max ?? null, prMax?.max ?? null, commitMax?.max ?? null].filter((value) => typeof value === 'number');
225
+ const dataAsOf = values.length > 0 ? Math.max(...values) : null;
226
+ const cacheAgeMinutes = dataAsOf !== null ? Math.floor((now - dataAsOf) / 60000) : null;
227
+ const syncedRows = this.db.query('SELECT COUNT(*) as count FROM sync_state').get();
228
+ return {
229
+ dataAsOf,
230
+ cacheAgeMinutes,
231
+ reposSynced: syncedRows.count,
232
+ totalRepos: repos.length,
233
+ };
234
+ }
235
+ isFresh(fetchedAt, ttlHours) {
236
+ return fetchedAt >= this.now() - ttlHours * 60 * 60 * 1000;
237
+ }
238
+ eventExists(event, timestamp) {
239
+ switch (event.type) {
240
+ case 'pr_opened':
241
+ case 'pr_ready':
242
+ case 'pr_merged':
243
+ case 'pr_closed':
244
+ case 'review_submitted':
245
+ case 'comment_added': {
246
+ const prNumber = 'pr_number' in event ? event.pr_number : null;
247
+ if (prNumber === null) {
248
+ return false;
249
+ }
250
+ const row = this.db
251
+ .query("SELECT 1 FROM events WHERE type = ? AND repo = ? AND timestamp = ? AND json_extract(payload, '$.pr_number') = ?")
252
+ .get(event.type, event.repo, timestamp, prNumber);
253
+ return Boolean(row);
254
+ }
255
+ case 'commit_pushed': {
256
+ const row = this.db
257
+ .query("SELECT 1 FROM events WHERE type = ? AND repo = ? AND json_extract(payload, '$.sha') = ?")
258
+ .get(event.type, event.repo, event.sha);
259
+ return Boolean(row);
260
+ }
261
+ default: {
262
+ const _exhaustive = event;
263
+ throw new Error(`Unhandled event type: ${_exhaustive}`);
264
+ }
265
+ }
266
+ }
267
+ }
268
+ exports.Cache = Cache;
269
+ function getEventTimestamp(event) {
270
+ switch (event.type) {
271
+ case 'pr_opened':
272
+ return event.opened_at;
273
+ case 'pr_ready':
274
+ return event.ready_at;
275
+ case 'pr_merged':
276
+ return event.merged_at;
277
+ case 'pr_closed':
278
+ return event.closed_at;
279
+ case 'review_submitted':
280
+ return event.submitted_at;
281
+ case 'comment_added':
282
+ return event.commented_at;
283
+ case 'commit_pushed':
284
+ return event.committed_at;
285
+ default: {
286
+ const _exhaustive = event;
287
+ throw new Error(`Unhandled event type: ${_exhaustive}`);
288
+ }
289
+ }
290
+ }
291
+ function isSqliteMemoryPath(dbPath) {
292
+ return dbPath === ':memory:' || dbPath === '';
293
+ }
294
+ async function ensureDir(dbPath) {
295
+ if (isSqliteMemoryPath(dbPath)) {
296
+ return;
297
+ }
298
+ const dir = node_path_1.default.dirname(dbPath);
299
+ await node_fs_1.promises.mkdir(dir, { recursive: true });
300
+ }
301
+ function toCommitFromEvent(event) {
302
+ return {
303
+ repo: event.repo,
304
+ sha: event.sha,
305
+ author: event.author,
306
+ committed_at: event.committed_at,
307
+ message: event.message,
308
+ additions: event.additions,
309
+ deletions: event.deletions,
310
+ files_changed: event.files_changed,
311
+ };
312
+ }
@@ -0,0 +1,58 @@
1
+ import type { Commit, CommitPushedEvent, Event, GitHubUsername, PullRequest, RepoFullName, Timestamp } from "./types.cjs";
2
+ export interface CacheFreshness {
3
+ dataAsOf: Timestamp | null;
4
+ cacheAgeMinutes: number | null;
5
+ reposSynced: number;
6
+ totalRepos: number;
7
+ }
8
+ export interface CacheOptions {
9
+ path: string;
10
+ now?: () => number;
11
+ }
12
+ export interface SyncState {
13
+ repo: RepoFullName;
14
+ last_sync: Timestamp;
15
+ cursor: string | null;
16
+ }
17
+ export declare class Cache {
18
+ private readonly db;
19
+ private readonly now;
20
+ private constructor();
21
+ static open(options: CacheOptions): Promise<Cache>;
22
+ init(): void;
23
+ close(): void;
24
+ insertEvent(event: Event, fetchedAt?: Timestamp): boolean;
25
+ upsertPullRequest(pr: PullRequest, fetchedAt?: Timestamp): void;
26
+ upsertCommit(commit: Commit, fetchedAt?: Timestamp): void;
27
+ getPullRequest(repo: RepoFullName, number: number): {
28
+ data: PullRequest;
29
+ fetched_at: Timestamp;
30
+ } | null;
31
+ getPullRequests(filter?: {
32
+ repo?: RepoFullName;
33
+ author?: GitHubUsername;
34
+ state?: string;
35
+ }): PullRequest[];
36
+ getCommits(filter?: {
37
+ repo?: RepoFullName;
38
+ author?: GitHubUsername;
39
+ }): Commit[];
40
+ getCommit(repo: RepoFullName, sha: string): {
41
+ data: Commit;
42
+ fetched_at: Timestamp;
43
+ } | null;
44
+ getEvents(filter?: {
45
+ repo?: RepoFullName;
46
+ type?: Event['type'];
47
+ since?: Timestamp;
48
+ until?: Timestamp;
49
+ }): Event[];
50
+ getEventsForPr(repo: RepoFullName, prNumber: number): Event[];
51
+ getSyncState(repo: RepoFullName): SyncState | null;
52
+ upsertSyncState(repo: RepoFullName, lastSync: Timestamp, cursor?: string | null): void;
53
+ getFreshness(repos: RepoFullName[]): CacheFreshness;
54
+ isFresh(fetchedAt: Timestamp, ttlHours: number): boolean;
55
+ private eventExists;
56
+ }
57
+ export declare function getEventTimestamp(event: Event): Timestamp;
58
+ export declare function toCommitFromEvent(event: CommitPushedEvent): Commit;
@@ -0,0 +1,58 @@
1
+ import type { Commit, CommitPushedEvent, Event, GitHubUsername, PullRequest, RepoFullName, Timestamp } from "./types.js";
2
+ export interface CacheFreshness {
3
+ dataAsOf: Timestamp | null;
4
+ cacheAgeMinutes: number | null;
5
+ reposSynced: number;
6
+ totalRepos: number;
7
+ }
8
+ export interface CacheOptions {
9
+ path: string;
10
+ now?: () => number;
11
+ }
12
+ export interface SyncState {
13
+ repo: RepoFullName;
14
+ last_sync: Timestamp;
15
+ cursor: string | null;
16
+ }
17
+ export declare class Cache {
18
+ private readonly db;
19
+ private readonly now;
20
+ private constructor();
21
+ static open(options: CacheOptions): Promise<Cache>;
22
+ init(): void;
23
+ close(): void;
24
+ insertEvent(event: Event, fetchedAt?: Timestamp): boolean;
25
+ upsertPullRequest(pr: PullRequest, fetchedAt?: Timestamp): void;
26
+ upsertCommit(commit: Commit, fetchedAt?: Timestamp): void;
27
+ getPullRequest(repo: RepoFullName, number: number): {
28
+ data: PullRequest;
29
+ fetched_at: Timestamp;
30
+ } | null;
31
+ getPullRequests(filter?: {
32
+ repo?: RepoFullName;
33
+ author?: GitHubUsername;
34
+ state?: string;
35
+ }): PullRequest[];
36
+ getCommits(filter?: {
37
+ repo?: RepoFullName;
38
+ author?: GitHubUsername;
39
+ }): Commit[];
40
+ getCommit(repo: RepoFullName, sha: string): {
41
+ data: Commit;
42
+ fetched_at: Timestamp;
43
+ } | null;
44
+ getEvents(filter?: {
45
+ repo?: RepoFullName;
46
+ type?: Event['type'];
47
+ since?: Timestamp;
48
+ until?: Timestamp;
49
+ }): Event[];
50
+ getEventsForPr(repo: RepoFullName, prNumber: number): Event[];
51
+ getSyncState(repo: RepoFullName): SyncState | null;
52
+ upsertSyncState(repo: RepoFullName, lastSync: Timestamp, cursor?: string | null): void;
53
+ getFreshness(repos: RepoFullName[]): CacheFreshness;
54
+ isFresh(fetchedAt: Timestamp, ttlHours: number): boolean;
55
+ private eventExists;
56
+ }
57
+ export declare function getEventTimestamp(event: Event): Timestamp;
58
+ export declare function toCommitFromEvent(event: CommitPushedEvent): Commit;