@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/sync.js ADDED
@@ -0,0 +1,357 @@
1
+ import { toCommitFromEvent } from "./cache.js";
2
+ export async function syncRepos(options) {
3
+ const now = Date.now();
4
+ const syncTtlMs = options.config.cache.sync_ttl_hours * 60 * 60 * 1000;
5
+ const concurrency = options.config.cache.concurrency;
6
+ // Filter repos that need syncing
7
+ const reposToSync = [];
8
+ const skippedRepos = [];
9
+ for (const repo of options.repos) {
10
+ if (options.forceSync) {
11
+ reposToSync.push(repo);
12
+ continue;
13
+ }
14
+ const syncState = options.cache.getSyncState(repo);
15
+ if (syncState && now - syncState.last_sync < syncTtlMs) {
16
+ skippedRepos.push(repo);
17
+ }
18
+ else {
19
+ reposToSync.push(repo);
20
+ }
21
+ }
22
+ if (skippedRepos.length > 0) {
23
+ options.onProgress?.({
24
+ phase: 'sync',
25
+ message: `Skipping ${skippedRepos.length} repos (synced within ${options.config.cache.sync_ttl_hours}h)`,
26
+ });
27
+ }
28
+ if (reposToSync.length === 0) {
29
+ options.onProgress?.({
30
+ phase: 'sync',
31
+ message: 'All repos up to date',
32
+ });
33
+ return {
34
+ synced_at: now,
35
+ repos: options.repos,
36
+ events_inserted: 0,
37
+ pull_requests_upserted: 0,
38
+ commits_upserted: 0,
39
+ };
40
+ }
41
+ let eventsInserted = 0;
42
+ let prsUpserted = 0;
43
+ let commitsUpserted = 0;
44
+ let completed = 0;
45
+ // Process repos in parallel with concurrency limit
46
+ const processRepo = async (repo) => {
47
+ const repoResult = await syncRepo(repo, options);
48
+ eventsInserted += repoResult.eventsInserted;
49
+ prsUpserted += repoResult.prsUpserted;
50
+ commitsUpserted += repoResult.commitsUpserted;
51
+ options.cache.upsertSyncState(repo, now, null);
52
+ completed += 1;
53
+ options.onProgress?.({
54
+ phase: 'sync',
55
+ message: `Synced ${repo} (${repoResult.prsUpserted} PRs, ${repoResult.commitsUpserted} commits)`,
56
+ current: completed,
57
+ total: reposToSync.length,
58
+ });
59
+ };
60
+ // Run with concurrency limit
61
+ await runWithConcurrency(reposToSync, processRepo, concurrency);
62
+ return {
63
+ synced_at: now,
64
+ repos: options.repos,
65
+ events_inserted: eventsInserted,
66
+ pull_requests_upserted: prsUpserted,
67
+ commits_upserted: commitsUpserted,
68
+ };
69
+ }
70
+ async function runWithConcurrency(items, fn, concurrency) {
71
+ if (!Number.isFinite(concurrency) || concurrency <= 0) {
72
+ throw new Error(`Invalid concurrency: ${concurrency}`);
73
+ }
74
+ if (items.length === 0) {
75
+ return;
76
+ }
77
+ const queue = [...items];
78
+ const running = new Set();
79
+ let firstError = null;
80
+ while (queue.length > 0 || running.size > 0) {
81
+ while (!firstError && running.size < concurrency && queue.length > 0) {
82
+ const item = queue.shift();
83
+ if (item === undefined) {
84
+ break;
85
+ }
86
+ const promise = Promise.resolve().then(() => fn(item));
87
+ const tracked = promise.finally(() => {
88
+ running.delete(tracked);
89
+ });
90
+ running.add(tracked);
91
+ }
92
+ if (running.size > 0) {
93
+ try {
94
+ await Promise.race(running);
95
+ }
96
+ catch (error) {
97
+ if (!firstError) {
98
+ firstError = error;
99
+ queue.length = 0;
100
+ }
101
+ }
102
+ }
103
+ }
104
+ if (firstError) {
105
+ throw firstError;
106
+ }
107
+ }
108
+ async function syncRepo(repo, options) {
109
+ const { cache, github, config, timeRange } = options;
110
+ const now = Date.now();
111
+ let eventsInserted = 0;
112
+ let prsUpserted = 0;
113
+ let commitsUpserted = 0;
114
+ const pullRequests = await github.listPullRequests(repo);
115
+ for (const prListItem of pullRequests) {
116
+ const updatedAt = Date.parse(prListItem.updated_at);
117
+ if (prListItem.state !== 'open' && updatedAt < timeRange.start) {
118
+ continue;
119
+ }
120
+ const existing = cache.getPullRequest(repo, prListItem.number);
121
+ const shouldFetch = !existing || !cache.isFresh(existing.fetched_at, config.cache.ttl_hours);
122
+ if (shouldFetch) {
123
+ const prDetail = await github.getPullRequest(repo, prListItem.number);
124
+ const pr = toPullRequest(repo, prDetail);
125
+ cache.upsertPullRequest(pr, now);
126
+ prsUpserted += 1;
127
+ eventsInserted += insertPrEvents(cache, pr, prDetail.merged_by?.login ?? pr.author);
128
+ eventsInserted += await insertTimelineEvents(cache, github, pr);
129
+ eventsInserted += await insertReviewEvents(cache, github, pr);
130
+ eventsInserted += await insertCommentEvents(cache, github, pr);
131
+ }
132
+ else if (existing) {
133
+ eventsInserted += insertPrEvents(cache, existing.data);
134
+ }
135
+ }
136
+ const commitList = await github.listCommits(repo, {
137
+ since: new Date(timeRange.start).toISOString(),
138
+ until: new Date(timeRange.end).toISOString(),
139
+ });
140
+ for (const commitItem of commitList) {
141
+ const sha = commitItem.sha;
142
+ const existingCommit = cache.getCommit(repo, sha);
143
+ const shouldFetch = !existingCommit || !cache.isFresh(existingCommit.fetched_at, config.cache.ttl_hours);
144
+ let commitDetail = null;
145
+ if (shouldFetch) {
146
+ commitDetail = await github.getCommit(repo, sha);
147
+ }
148
+ if (!commitDetail && existingCommit) {
149
+ const commitEvent = toCommitEventFromCommit(repo, existingCommit.data);
150
+ const inserted = cache.insertEvent(commitEvent, now);
151
+ if (inserted) {
152
+ eventsInserted += 1;
153
+ }
154
+ continue;
155
+ }
156
+ const commitEvent = toCommitEvent(repo, commitDetail);
157
+ const inserted = cache.insertEvent(commitEvent, now);
158
+ if (inserted) {
159
+ eventsInserted += 1;
160
+ }
161
+ cache.upsertCommit(toCommitFromEvent(commitEvent), now);
162
+ commitsUpserted += 1;
163
+ }
164
+ return { eventsInserted, prsUpserted, commitsUpserted };
165
+ }
166
+ function insertPrEvents(cache, pr, mergedBy) {
167
+ let count = 0;
168
+ const openedEvent = {
169
+ type: 'pr_opened',
170
+ repo: pr.repo,
171
+ pr_number: pr.number,
172
+ author: pr.author,
173
+ opened_at: pr.created_at,
174
+ is_draft: pr.draft,
175
+ };
176
+ if (cache.insertEvent(openedEvent)) {
177
+ count += 1;
178
+ }
179
+ if (pr.merged_at) {
180
+ const mergedEvent = {
181
+ type: 'pr_merged',
182
+ repo: pr.repo,
183
+ pr_number: pr.number,
184
+ merged_at: pr.merged_at,
185
+ merged_by: mergedBy ?? pr.author,
186
+ };
187
+ if (cache.insertEvent(mergedEvent)) {
188
+ count += 1;
189
+ }
190
+ }
191
+ else if (pr.closed_at) {
192
+ const closedEvent = {
193
+ type: 'pr_closed',
194
+ repo: pr.repo,
195
+ pr_number: pr.number,
196
+ closed_at: pr.closed_at,
197
+ };
198
+ if (cache.insertEvent(closedEvent)) {
199
+ count += 1;
200
+ }
201
+ }
202
+ return count;
203
+ }
204
+ async function insertTimelineEvents(cache, github, pr) {
205
+ let count = 0;
206
+ const events = await github.listIssueEvents(pr.repo, pr.number);
207
+ for (const event of events) {
208
+ if (event.event === 'ready_for_review') {
209
+ const readyEvent = {
210
+ type: 'pr_ready',
211
+ repo: pr.repo,
212
+ pr_number: pr.number,
213
+ ready_at: Date.parse(event.created_at),
214
+ };
215
+ if (cache.insertEvent(readyEvent)) {
216
+ count += 1;
217
+ }
218
+ }
219
+ if (event.event === 'reopened') {
220
+ const reopenedEvent = {
221
+ type: 'pr_opened',
222
+ repo: pr.repo,
223
+ pr_number: pr.number,
224
+ author: pr.author,
225
+ opened_at: Date.parse(event.created_at),
226
+ is_draft: false,
227
+ };
228
+ if (cache.insertEvent(reopenedEvent)) {
229
+ count += 1;
230
+ }
231
+ }
232
+ if (event.event === 'closed' && pr.merged_at === null) {
233
+ const closedEvent = {
234
+ type: 'pr_closed',
235
+ repo: pr.repo,
236
+ pr_number: pr.number,
237
+ closed_at: Date.parse(event.created_at),
238
+ };
239
+ if (cache.insertEvent(closedEvent)) {
240
+ count += 1;
241
+ }
242
+ }
243
+ }
244
+ return count;
245
+ }
246
+ async function insertReviewEvents(cache, github, pr) {
247
+ let count = 0;
248
+ const reviews = await github.listPullRequestReviews(pr.repo, pr.number);
249
+ for (const review of reviews) {
250
+ const reviewer = review.user?.login;
251
+ if (!reviewer || !review.submitted_at) {
252
+ continue;
253
+ }
254
+ const reviewEvent = {
255
+ type: 'review_submitted',
256
+ repo: pr.repo,
257
+ pr_number: pr.number,
258
+ reviewer,
259
+ state: normalizeReviewState(review.state),
260
+ submitted_at: Date.parse(review.submitted_at),
261
+ };
262
+ if (cache.insertEvent(reviewEvent)) {
263
+ count += 1;
264
+ }
265
+ }
266
+ return count;
267
+ }
268
+ async function insertCommentEvents(cache, github, pr) {
269
+ let count = 0;
270
+ const comments = await github.listIssueComments(pr.repo, pr.number);
271
+ for (const comment of comments) {
272
+ const author = comment.user?.login;
273
+ if (!author) {
274
+ continue;
275
+ }
276
+ const commentEvent = {
277
+ type: 'comment_added',
278
+ repo: pr.repo,
279
+ pr_number: pr.number,
280
+ author,
281
+ commented_at: Date.parse(comment.created_at),
282
+ };
283
+ if (cache.insertEvent(commentEvent)) {
284
+ count += 1;
285
+ }
286
+ }
287
+ return count;
288
+ }
289
+ function toPullRequest(repo, pr) {
290
+ return {
291
+ repo,
292
+ number: pr.number,
293
+ title: pr.title,
294
+ state: pr.merged_at ? 'merged' : pr.state,
295
+ draft: pr.draft ?? false,
296
+ author: pr.user?.login ?? 'unknown',
297
+ created_at: Date.parse(pr.created_at),
298
+ updated_at: Date.parse(pr.updated_at),
299
+ merged_at: pr.merged_at ? Date.parse(pr.merged_at) : null,
300
+ closed_at: pr.closed_at ? Date.parse(pr.closed_at) : null,
301
+ additions: pr.additions ?? 0,
302
+ deletions: pr.deletions ?? 0,
303
+ commits: pr.commits ?? 0,
304
+ requested_reviewers: (pr.requested_reviewers ?? []).map((reviewer) => reviewer.login ?? 'unknown'),
305
+ labels: (pr.labels ?? []).map((label) => typeof label === 'string' ? label : (label.name ?? 'unknown')),
306
+ linked_issues: [],
307
+ };
308
+ }
309
+ function toCommitEvent(repo, commit) {
310
+ const author = commit.author?.login ?? commit.commit.author?.name ?? 'unknown';
311
+ const message = commit.commit.message ?? '';
312
+ const stats = commit.stats ?? { additions: 0, deletions: 0, total: 0 };
313
+ const filesChanged = commit.files ? commit.files.length : 0;
314
+ const committedAt = commit.commit.author?.date
315
+ ? Date.parse(commit.commit.author.date)
316
+ : Date.now();
317
+ return {
318
+ type: 'commit_pushed',
319
+ repo,
320
+ sha: commit.sha,
321
+ author,
322
+ committed_at: committedAt,
323
+ message,
324
+ additions: stats.additions ?? 0,
325
+ deletions: stats.deletions ?? 0,
326
+ files_changed: filesChanged,
327
+ };
328
+ }
329
+ function toCommitEventFromCommit(repo, commit) {
330
+ return {
331
+ type: 'commit_pushed',
332
+ repo,
333
+ sha: commit.sha,
334
+ author: commit.author,
335
+ committed_at: commit.committed_at,
336
+ message: commit.message,
337
+ additions: commit.additions,
338
+ deletions: commit.deletions,
339
+ files_changed: commit.files_changed,
340
+ };
341
+ }
342
+ function normalizeReviewState(state) {
343
+ switch (state) {
344
+ case 'approved':
345
+ return 'approved';
346
+ case 'changes_requested':
347
+ return 'changes_requested';
348
+ case 'commented':
349
+ return 'commented';
350
+ case 'dismissed':
351
+ return 'dismissed';
352
+ case 'pending':
353
+ return 'pending';
354
+ default:
355
+ return 'commented';
356
+ }
357
+ }
package/dist/team.cjs ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveTeamMembers = resolveTeamMembers;
4
+ async function resolveTeamMembers(options) {
5
+ const strategy = options.config.team.strategy;
6
+ switch (strategy) {
7
+ case 'config_list':
8
+ return unique(options.config.team.members);
9
+ case 'org_members': {
10
+ const orgs = unique([...options.config.orgs, ...(options.orgs ?? [])]);
11
+ const members = await Promise.all(orgs.map((org) => listOwnerMembers(options.github, org)));
12
+ return unique(members.flat());
13
+ }
14
+ case 'repo_contributors': {
15
+ const contributors = await Promise.all(options.repos.map((repo) => options.github.listContributors(repo)));
16
+ return unique(contributors.flat());
17
+ }
18
+ default: {
19
+ const _exhaustive = strategy;
20
+ throw new Error(`Unhandled team strategy: ${_exhaustive}`);
21
+ }
22
+ }
23
+ }
24
+ function unique(values) {
25
+ return Array.from(new Set(values));
26
+ }
27
+ async function listOwnerMembers(github, owner) {
28
+ try {
29
+ return await github.listOrgMembers(owner);
30
+ }
31
+ catch (error) {
32
+ const status = getStatus(error);
33
+ if (status === 404) {
34
+ // Owner is a user, not an org - return the user as sole "member"
35
+ return [owner];
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+ function getStatus(error) {
41
+ if (typeof error === 'object' && error && 'status' in error) {
42
+ return error.status;
43
+ }
44
+ return undefined;
45
+ }
@@ -0,0 +1,10 @@
1
+ import type { Config } from "./config.cjs";
2
+ import type { GitHubClient } from "./github.cjs";
3
+ import type { GitHubUsername, RepoFullName } from "./types.cjs";
4
+ export interface ResolveTeamOptions {
5
+ config: Config;
6
+ github: GitHubClient;
7
+ repos: RepoFullName[];
8
+ orgs?: string[];
9
+ }
10
+ export declare function resolveTeamMembers(options: ResolveTeamOptions): Promise<GitHubUsername[]>;
package/dist/team.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Config } from "./config.js";
2
+ import type { GitHubClient } from "./github.js";
3
+ import type { GitHubUsername, RepoFullName } from "./types.js";
4
+ export interface ResolveTeamOptions {
5
+ config: Config;
6
+ github: GitHubClient;
7
+ repos: RepoFullName[];
8
+ orgs?: string[];
9
+ }
10
+ export declare function resolveTeamMembers(options: ResolveTeamOptions): Promise<GitHubUsername[]>;
package/dist/team.js ADDED
@@ -0,0 +1,42 @@
1
+ export async function resolveTeamMembers(options) {
2
+ const strategy = options.config.team.strategy;
3
+ switch (strategy) {
4
+ case 'config_list':
5
+ return unique(options.config.team.members);
6
+ case 'org_members': {
7
+ const orgs = unique([...options.config.orgs, ...(options.orgs ?? [])]);
8
+ const members = await Promise.all(orgs.map((org) => listOwnerMembers(options.github, org)));
9
+ return unique(members.flat());
10
+ }
11
+ case 'repo_contributors': {
12
+ const contributors = await Promise.all(options.repos.map((repo) => options.github.listContributors(repo)));
13
+ return unique(contributors.flat());
14
+ }
15
+ default: {
16
+ const _exhaustive = strategy;
17
+ throw new Error(`Unhandled team strategy: ${_exhaustive}`);
18
+ }
19
+ }
20
+ }
21
+ function unique(values) {
22
+ return Array.from(new Set(values));
23
+ }
24
+ async function listOwnerMembers(github, owner) {
25
+ try {
26
+ return await github.listOrgMembers(owner);
27
+ }
28
+ catch (error) {
29
+ const status = getStatus(error);
30
+ if (status === 404) {
31
+ // Owner is a user, not an org - return the user as sole "member"
32
+ return [owner];
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+ function getStatus(error) {
38
+ if (typeof error === 'object' && error && 'status' in error) {
39
+ return error.status;
40
+ }
41
+ return undefined;
42
+ }
package/dist/time.cjs ADDED
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.periodToRange = periodToRange;
4
+ exports.resolveSince = resolveSince;
5
+ exports.resolveUntil = resolveUntil;
6
+ exports.formatTimestamp = formatTimestamp;
7
+ exports.diffInDays = diffInDays;
8
+ exports.parseRelativeDuration = parseRelativeDuration;
9
+ const DAY_MS = 24 * 60 * 60 * 1000;
10
+ function periodToRange(period, now, timeZone) {
11
+ const end = now.getTime();
12
+ switch (period) {
13
+ case 'today': {
14
+ const start = startOfDay(now, timeZone);
15
+ return { start, end, label: 'today' };
16
+ }
17
+ case 'week': {
18
+ const start = end - 7 * DAY_MS;
19
+ return { start, end, label: 'last 7 days' };
20
+ }
21
+ case 'sprint': {
22
+ const start = end - 14 * DAY_MS;
23
+ return { start, end, label: 'last 14 days' };
24
+ }
25
+ case 'month': {
26
+ const start = end - 30 * DAY_MS;
27
+ return { start, end, label: 'last 30 days' };
28
+ }
29
+ case 'quarter': {
30
+ const start = end - 90 * DAY_MS;
31
+ return { start, end, label: 'last 90 days' };
32
+ }
33
+ default: {
34
+ const _exhaustive = period;
35
+ throw new Error(`Unhandled period: ${_exhaustive}`);
36
+ }
37
+ }
38
+ }
39
+ function resolveSince(input, now) {
40
+ if (isRelative(input)) {
41
+ return now.getTime() - parseRelativeDuration(input);
42
+ }
43
+ return parseAbsoluteDate(input);
44
+ }
45
+ function resolveUntil(input, now) {
46
+ if (isRelative(input)) {
47
+ return now.getTime() - parseRelativeDuration(input);
48
+ }
49
+ return parseAbsoluteDate(input);
50
+ }
51
+ function formatTimestamp(timestamp, timeZone) {
52
+ const formatter = new Intl.DateTimeFormat('en-US', {
53
+ timeZone,
54
+ year: 'numeric',
55
+ month: '2-digit',
56
+ day: '2-digit',
57
+ hour: '2-digit',
58
+ minute: '2-digit',
59
+ second: '2-digit',
60
+ hour12: false,
61
+ });
62
+ return formatter.format(new Date(timestamp));
63
+ }
64
+ function diffInDays(start, end) {
65
+ return Math.floor((end - start) / DAY_MS);
66
+ }
67
+ function parseRelativeDuration(input) {
68
+ const match = input.trim().match(/^(\d+)([dwmy])$/i);
69
+ if (!match) {
70
+ throw new Error(`Invalid relative duration: ${input}`);
71
+ }
72
+ const value = Number(match[1]);
73
+ const unitRaw = match[2];
74
+ if (!unitRaw) {
75
+ throw new Error(`Invalid relative duration unit: ${input}`);
76
+ }
77
+ const unit = unitRaw.toLowerCase();
78
+ switch (unit) {
79
+ case 'd':
80
+ return value * DAY_MS;
81
+ case 'w':
82
+ return value * 7 * DAY_MS;
83
+ case 'm':
84
+ return value * 30 * DAY_MS;
85
+ case 'y':
86
+ return value * 365 * DAY_MS;
87
+ default: {
88
+ const _exhaustive = unit;
89
+ throw new Error(`Unhandled duration unit: ${_exhaustive}`);
90
+ }
91
+ }
92
+ }
93
+ function isRelative(input) {
94
+ return /^(\d+)([dwmy])$/i.test(input.trim());
95
+ }
96
+ function parseAbsoluteDate(input) {
97
+ const parsed = Date.parse(input);
98
+ if (Number.isNaN(parsed)) {
99
+ throw new Error(`Invalid date: ${input}`);
100
+ }
101
+ return parsed;
102
+ }
103
+ function startOfDay(now, timeZone) {
104
+ const parts = getZonedParts(now, timeZone);
105
+ const utcMs = zonedTimeToUtcMs({
106
+ ...parts,
107
+ hour: 0,
108
+ minute: 0,
109
+ second: 0,
110
+ }, timeZone);
111
+ return utcMs;
112
+ }
113
+ function getZonedParts(date, timeZone) {
114
+ const formatter = new Intl.DateTimeFormat('en-US', {
115
+ timeZone,
116
+ hour12: false,
117
+ year: 'numeric',
118
+ month: '2-digit',
119
+ day: '2-digit',
120
+ hour: '2-digit',
121
+ minute: '2-digit',
122
+ second: '2-digit',
123
+ });
124
+ const parts = formatter.formatToParts(date);
125
+ const lookup = Object.fromEntries(parts.filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]));
126
+ const year = lookup.year;
127
+ const month = lookup.month;
128
+ const day = lookup.day;
129
+ const hour = lookup.hour;
130
+ const minute = lookup.minute;
131
+ const second = lookup.second;
132
+ if (!year || !month || !day || !hour || !minute || !second) {
133
+ throw new Error(`Failed to resolve timezone parts for ${timeZone}`);
134
+ }
135
+ return {
136
+ year: Number(year),
137
+ month: Number(month),
138
+ day: Number(day),
139
+ hour: Number(hour),
140
+ minute: Number(minute),
141
+ second: Number(second),
142
+ };
143
+ }
144
+ function zonedTimeToUtcMs(parts, timeZone) {
145
+ const utcGuess = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
146
+ const offset = getTimeZoneOffsetMs(new Date(utcGuess), timeZone);
147
+ return utcGuess - offset;
148
+ }
149
+ function getTimeZoneOffsetMs(date, timeZone) {
150
+ const parts = getZonedParts(date, timeZone);
151
+ const utcFromParts = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
152
+ return utcFromParts - date.getTime();
153
+ }
@@ -0,0 +1,13 @@
1
+ import type { Timestamp } from "./types.cjs";
2
+ export type Period = 'today' | 'week' | 'sprint' | 'month' | 'quarter';
3
+ export interface TimeRange {
4
+ start: Timestamp;
5
+ end: Timestamp;
6
+ label: string;
7
+ }
8
+ export declare function periodToRange(period: Period, now: Date, timeZone: string): TimeRange;
9
+ export declare function resolveSince(input: string, now: Date): Timestamp;
10
+ export declare function resolveUntil(input: string, now: Date): Timestamp;
11
+ export declare function formatTimestamp(timestamp: Timestamp, timeZone: string): string;
12
+ export declare function diffInDays(start: Timestamp, end: Timestamp): number;
13
+ export declare function parseRelativeDuration(input: string): number;
package/dist/time.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Timestamp } from "./types.js";
2
+ export type Period = 'today' | 'week' | 'sprint' | 'month' | 'quarter';
3
+ export interface TimeRange {
4
+ start: Timestamp;
5
+ end: Timestamp;
6
+ label: string;
7
+ }
8
+ export declare function periodToRange(period: Period, now: Date, timeZone: string): TimeRange;
9
+ export declare function resolveSince(input: string, now: Date): Timestamp;
10
+ export declare function resolveUntil(input: string, now: Date): Timestamp;
11
+ export declare function formatTimestamp(timestamp: Timestamp, timeZone: string): string;
12
+ export declare function diffInDays(start: Timestamp, end: Timestamp): number;
13
+ export declare function parseRelativeDuration(input: string): number;