aiblueprint-cli 1.4.13 → 1.4.15
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-code-config/scripts/CLAUDE.md +50 -0
- package/claude-code-config/scripts/biome.json +37 -0
- package/claude-code-config/scripts/bun.lockb +0 -0
- package/claude-code-config/scripts/package.json +39 -0
- package/claude-code-config/scripts/statusline/__tests__/context.test.ts +229 -0
- package/claude-code-config/scripts/statusline/__tests__/formatters.test.ts +108 -0
- package/claude-code-config/scripts/statusline/__tests__/statusline.test.ts +309 -0
- package/claude-code-config/scripts/statusline/data/.gitignore +8 -0
- package/claude-code-config/scripts/statusline/data/.gitkeep +0 -0
- package/claude-code-config/scripts/statusline/defaults.json +4 -0
- package/claude-code-config/scripts/statusline/docs/ARCHITECTURE.md +166 -0
- package/claude-code-config/scripts/statusline/fixtures/mock-transcript.jsonl +4 -0
- package/claude-code-config/scripts/statusline/fixtures/test-input.json +35 -0
- package/claude-code-config/scripts/statusline/src/index.ts +74 -0
- package/claude-code-config/scripts/statusline/src/lib/config-types.ts +4 -0
- package/claude-code-config/scripts/statusline/src/lib/menu-factories.ts +224 -0
- package/claude-code-config/scripts/statusline/src/lib/presets.ts +177 -0
- package/claude-code-config/scripts/statusline/src/lib/render-pure.ts +341 -21
- package/claude-code-config/scripts/statusline/src/lib/utils.ts +15 -0
- package/claude-code-config/scripts/statusline/src/tests/spend-v2.test.ts +306 -0
- package/claude-code-config/scripts/statusline/statusline.config.json +25 -39
- package/claude-code-config/scripts/statusline/test-with-fixtures.ts +37 -0
- package/claude-code-config/scripts/statusline/test.ts +20 -0
- package/claude-code-config/scripts/tsconfig.json +27 -0
- package/dist/cli.js +16 -11
- package/package.json +1 -1
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ARCHITECTURE: Raw data in, formatted string out.
|
|
5
5
|
* ALL config decisions happen here, not in data preparation.
|
|
6
|
-
*
|
|
7
|
-
* FREE VERSION: Simplified - No limits, weekly, or daily tracking
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
import type { StatuslineConfig } from "./config-types";
|
|
@@ -14,13 +12,52 @@ import {
|
|
|
14
12
|
formatDuration,
|
|
15
13
|
formatPath,
|
|
16
14
|
formatProgressBar,
|
|
15
|
+
formatResetTime,
|
|
17
16
|
formatTokens,
|
|
18
17
|
} from "./formatters";
|
|
19
18
|
|
|
19
|
+
const WEEKLY_HOURS = 168; // 7 days * 24 hours
|
|
20
|
+
const FIVE_HOUR_MINUTES = 300; // 5 hours * 60 minutes
|
|
21
|
+
|
|
20
22
|
// ─────────────────────────────────────────────────────────────
|
|
21
|
-
// DATA TYPES -
|
|
23
|
+
// RAW DATA TYPES - No pre-formatting, just raw values
|
|
22
24
|
// ─────────────────────────────────────────────────────────────
|
|
23
25
|
|
|
26
|
+
export interface GitChanges {
|
|
27
|
+
files: number;
|
|
28
|
+
added: number;
|
|
29
|
+
deleted: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RawGitData {
|
|
33
|
+
branch: string;
|
|
34
|
+
dirty: boolean;
|
|
35
|
+
staged: GitChanges;
|
|
36
|
+
unstaged: GitChanges;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UsageLimit {
|
|
40
|
+
utilization: number;
|
|
41
|
+
resets_at: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RawStatuslineData {
|
|
45
|
+
git: RawGitData | null;
|
|
46
|
+
path: string;
|
|
47
|
+
modelName: string;
|
|
48
|
+
cost: number;
|
|
49
|
+
durationMs: number;
|
|
50
|
+
contextTokens: number | null;
|
|
51
|
+
contextPercentage: number | null;
|
|
52
|
+
usageLimits?: {
|
|
53
|
+
five_hour: UsageLimit | null;
|
|
54
|
+
seven_day: UsageLimit | null;
|
|
55
|
+
};
|
|
56
|
+
periodCost?: number;
|
|
57
|
+
todayCost?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Legacy interface for backwards compatibility
|
|
24
61
|
export interface StatuslineData {
|
|
25
62
|
branch: string;
|
|
26
63
|
dirPath: string;
|
|
@@ -29,12 +66,63 @@ export interface StatuslineData {
|
|
|
29
66
|
sessionDuration: string;
|
|
30
67
|
contextTokens: number | null;
|
|
31
68
|
contextPercentage: number | null;
|
|
69
|
+
usageLimits?: {
|
|
70
|
+
five_hour: UsageLimit | null;
|
|
71
|
+
seven_day: UsageLimit | null;
|
|
72
|
+
};
|
|
73
|
+
periodCost?: number;
|
|
74
|
+
todayCost?: number;
|
|
32
75
|
}
|
|
33
76
|
|
|
34
77
|
// ─────────────────────────────────────────────────────────────
|
|
35
78
|
// FORMATTING - All config-aware formatting in one place
|
|
36
79
|
// ─────────────────────────────────────────────────────────────
|
|
37
80
|
|
|
81
|
+
function formatGitPart(
|
|
82
|
+
git: RawGitData | null,
|
|
83
|
+
config: StatuslineConfig["git"],
|
|
84
|
+
): string {
|
|
85
|
+
if (!git || !config.enabled) return "";
|
|
86
|
+
|
|
87
|
+
const parts: string[] = [];
|
|
88
|
+
|
|
89
|
+
if (config.showBranch) {
|
|
90
|
+
parts.push(colors.lightGray(git.branch));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (git.dirty && config.showDirtyIndicator) {
|
|
94
|
+
// Append to branch name without space
|
|
95
|
+
if (parts.length > 0) {
|
|
96
|
+
parts[parts.length - 1] += colors.purple("*");
|
|
97
|
+
} else {
|
|
98
|
+
parts.push(colors.purple("*"));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const changeParts: string[] = [];
|
|
103
|
+
|
|
104
|
+
if (config.showChanges) {
|
|
105
|
+
const totalAdded = git.staged.added + git.unstaged.added;
|
|
106
|
+
const totalDeleted = git.staged.deleted + git.unstaged.deleted;
|
|
107
|
+
if (totalAdded > 0) changeParts.push(colors.green(`+${totalAdded}`));
|
|
108
|
+
if (totalDeleted > 0) changeParts.push(colors.red(`-${totalDeleted}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (config.showStaged && git.staged.files > 0) {
|
|
112
|
+
changeParts.push(colors.gray(`~${git.staged.files}`));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (config.showUnstaged && git.unstaged.files > 0) {
|
|
116
|
+
changeParts.push(colors.yellow(`~${git.unstaged.files}`));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (changeParts.length > 0) {
|
|
120
|
+
parts.push(changeParts.join(" "));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return parts.join(" ");
|
|
124
|
+
}
|
|
125
|
+
|
|
38
126
|
function formatSessionPart(
|
|
39
127
|
cost: number,
|
|
40
128
|
durationMs: number,
|
|
@@ -106,12 +194,179 @@ function formatSessionPart(
|
|
|
106
194
|
return `${colors.gray("S:")} ${items.join(sep)}`;
|
|
107
195
|
}
|
|
108
196
|
|
|
197
|
+
function formatLimitsPart(
|
|
198
|
+
fiveHour: UsageLimit | null,
|
|
199
|
+
periodCost: number,
|
|
200
|
+
config: StatuslineConfig["limits"],
|
|
201
|
+
): string {
|
|
202
|
+
if (!config.enabled || !fiveHour) return "";
|
|
203
|
+
|
|
204
|
+
const parts: string[] = [];
|
|
205
|
+
|
|
206
|
+
if (config.cost.enabled && periodCost > 0) {
|
|
207
|
+
parts.push(
|
|
208
|
+
`${colors.gray("$")}${colors.dimWhite(formatCost(periodCost, config.cost.format))}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (config.percentage.enabled) {
|
|
213
|
+
if (config.percentage.progressBar.enabled) {
|
|
214
|
+
parts.push(
|
|
215
|
+
formatProgressBar({
|
|
216
|
+
percentage: fiveHour.utilization,
|
|
217
|
+
length: config.percentage.progressBar.length,
|
|
218
|
+
style: config.percentage.progressBar.style,
|
|
219
|
+
colorMode: config.percentage.progressBar.color,
|
|
220
|
+
background: config.percentage.progressBar.background,
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (config.percentage.showValue) {
|
|
226
|
+
parts.push(
|
|
227
|
+
`${colors.lightGray(fiveHour.utilization.toString())}${colors.gray("%")}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (config.showPacingDelta && fiveHour.resets_at) {
|
|
233
|
+
const delta = calculateFiveHourDelta(
|
|
234
|
+
fiveHour.utilization,
|
|
235
|
+
fiveHour.resets_at,
|
|
236
|
+
);
|
|
237
|
+
parts.push(
|
|
238
|
+
`${colors.gray("(")}${formatPacingDelta(delta)}${colors.gray(")")}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (config.showTimeLeft && fiveHour.resets_at) {
|
|
243
|
+
parts.push(colors.gray(`(${formatResetTime(fiveHour.resets_at)})`));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return parts.length > 0 ? `${colors.gray("L:")} ${parts.join(" ")}` : "";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function shouldShowWeekly(
|
|
250
|
+
config: StatuslineConfig["weeklyUsage"],
|
|
251
|
+
fiveHourUtilization: number | null,
|
|
252
|
+
): boolean {
|
|
253
|
+
if (config.enabled === true) return true;
|
|
254
|
+
if (config.enabled === false) return false;
|
|
255
|
+
if (config.enabled === "90%" && fiveHourUtilization !== null) {
|
|
256
|
+
return fiveHourUtilization >= 90;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function calculateWeeklyDelta(
|
|
262
|
+
utilization: number,
|
|
263
|
+
resetsAt: string | null,
|
|
264
|
+
): number {
|
|
265
|
+
if (!resetsAt) return 0;
|
|
266
|
+
|
|
267
|
+
const resetDate = new Date(resetsAt);
|
|
268
|
+
const now = new Date();
|
|
269
|
+
const diffMs = resetDate.getTime() - now.getTime();
|
|
270
|
+
const hoursRemaining = Math.max(0, diffMs / 3600000);
|
|
271
|
+
const timeElapsedPercent =
|
|
272
|
+
((WEEKLY_HOURS - hoursRemaining) / WEEKLY_HOURS) * 100;
|
|
273
|
+
|
|
274
|
+
return utilization - timeElapsedPercent;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function formatPacingDelta(delta: number): string {
|
|
278
|
+
const sign = delta >= 0 ? "+" : "";
|
|
279
|
+
const value = `${sign}${delta.toFixed(1)}%`;
|
|
280
|
+
|
|
281
|
+
if (delta > 5) return colors.green(value);
|
|
282
|
+
if (delta > 0) return colors.lightGray(value);
|
|
283
|
+
if (delta > -10) return colors.yellow(value);
|
|
284
|
+
return colors.red(value);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function calculateFiveHourDelta(
|
|
288
|
+
utilization: number,
|
|
289
|
+
resetsAt: string | null,
|
|
290
|
+
): number {
|
|
291
|
+
if (!resetsAt) return 0;
|
|
292
|
+
|
|
293
|
+
const resetDate = new Date(resetsAt);
|
|
294
|
+
const now = new Date();
|
|
295
|
+
const diffMs = resetDate.getTime() - now.getTime();
|
|
296
|
+
const minutesRemaining = Math.max(0, diffMs / 60000);
|
|
297
|
+
const timeElapsedPercent =
|
|
298
|
+
((FIVE_HOUR_MINUTES - minutesRemaining) / FIVE_HOUR_MINUTES) * 100;
|
|
299
|
+
|
|
300
|
+
return utilization - timeElapsedPercent;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function formatWeeklyPart(
|
|
304
|
+
sevenDay: UsageLimit | null,
|
|
305
|
+
fiveHourUtilization: number | null,
|
|
306
|
+
periodCost: number,
|
|
307
|
+
config: StatuslineConfig["weeklyUsage"],
|
|
308
|
+
): string {
|
|
309
|
+
if (!shouldShowWeekly(config, fiveHourUtilization) || !sevenDay) return "";
|
|
310
|
+
|
|
311
|
+
const parts: string[] = [];
|
|
312
|
+
|
|
313
|
+
if (config.cost.enabled && periodCost > 0) {
|
|
314
|
+
parts.push(
|
|
315
|
+
`${colors.gray("$")}${colors.dimWhite(formatCost(periodCost, config.cost.format))}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (config.percentage.enabled) {
|
|
320
|
+
if (config.percentage.progressBar.enabled) {
|
|
321
|
+
parts.push(
|
|
322
|
+
formatProgressBar({
|
|
323
|
+
percentage: sevenDay.utilization,
|
|
324
|
+
length: config.percentage.progressBar.length,
|
|
325
|
+
style: config.percentage.progressBar.style,
|
|
326
|
+
colorMode: config.percentage.progressBar.color,
|
|
327
|
+
background: config.percentage.progressBar.background,
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (config.percentage.showValue) {
|
|
333
|
+
parts.push(
|
|
334
|
+
`${colors.lightGray(sevenDay.utilization.toString())}${colors.gray("%")}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (config.showPacingDelta && sevenDay.resets_at) {
|
|
340
|
+
const delta = calculateWeeklyDelta(
|
|
341
|
+
sevenDay.utilization,
|
|
342
|
+
sevenDay.resets_at,
|
|
343
|
+
);
|
|
344
|
+
parts.push(
|
|
345
|
+
`${colors.gray("(")}${formatPacingDelta(delta)}${colors.gray(")")}`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (config.showTimeLeft && sevenDay.resets_at) {
|
|
350
|
+
parts.push(colors.gray(`(${formatResetTime(sevenDay.resets_at)})`));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return parts.length > 0 ? `${colors.gray("W:")} ${parts.join(" ")}` : "";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatDailyPart(
|
|
357
|
+
todayCost: number,
|
|
358
|
+
config: StatuslineConfig["dailySpend"],
|
|
359
|
+
): string {
|
|
360
|
+
if (!config.cost.enabled || todayCost <= 0) return "";
|
|
361
|
+
return `${colors.gray("D:")} ${colors.gray("$")}${colors.dimWhite(formatCost(todayCost, config.cost.format))}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
109
364
|
// ─────────────────────────────────────────────────────────────
|
|
110
|
-
// MAIN RENDER FUNCTION -
|
|
365
|
+
// MAIN RENDER FUNCTION - Raw data + config = output
|
|
111
366
|
// ─────────────────────────────────────────────────────────────
|
|
112
367
|
|
|
113
|
-
export function
|
|
114
|
-
data:
|
|
368
|
+
export function renderStatuslineRaw(
|
|
369
|
+
data: RawStatuslineData,
|
|
115
370
|
config: StatuslineConfig,
|
|
116
371
|
): string {
|
|
117
372
|
const sep = colors.gray(config.separator);
|
|
@@ -120,16 +375,12 @@ export function renderStatusline(
|
|
|
120
375
|
// Line 1: Git + Path + Model
|
|
121
376
|
const line1Parts: string[] = [];
|
|
122
377
|
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
line1Parts.push(colors.lightGray(data.branch));
|
|
126
|
-
}
|
|
378
|
+
const gitPart = formatGitPart(data.git, config.git);
|
|
379
|
+
if (gitPart) line1Parts.push(gitPart);
|
|
127
380
|
|
|
128
|
-
|
|
129
|
-
const pathPart = formatPath(data.dirPath, config.pathDisplayMode);
|
|
381
|
+
const pathPart = formatPath(data.path, config.pathDisplayMode);
|
|
130
382
|
line1Parts.push(colors.gray(pathPart));
|
|
131
383
|
|
|
132
|
-
// Model name (hide Sonnet if configured)
|
|
133
384
|
const isSonnet = data.modelName.toLowerCase().includes("sonnet");
|
|
134
385
|
if (!isSonnet || config.showSonnetModel) {
|
|
135
386
|
line1Parts.push(colors.peach(data.modelName));
|
|
@@ -137,21 +388,38 @@ export function renderStatusline(
|
|
|
137
388
|
|
|
138
389
|
sections.push(line1Parts.join(` ${sep} `));
|
|
139
390
|
|
|
140
|
-
// Line 2: Session info
|
|
141
|
-
const cost = parseFloat(data.sessionCost.replace(/[$,]/g, "")) || 0;
|
|
142
|
-
const durationMs = parseDurationToMs(data.sessionDuration);
|
|
143
|
-
|
|
391
|
+
// Line 2: Session info
|
|
144
392
|
const sessionPart = formatSessionPart(
|
|
145
|
-
cost,
|
|
146
|
-
durationMs,
|
|
393
|
+
data.cost,
|
|
394
|
+
data.durationMs,
|
|
147
395
|
data.contextTokens,
|
|
148
396
|
data.contextPercentage,
|
|
149
397
|
config.context.maxContextTokens,
|
|
150
398
|
config.session,
|
|
151
399
|
);
|
|
152
|
-
|
|
153
400
|
if (sessionPart) sections.push(sessionPart);
|
|
154
401
|
|
|
402
|
+
// Limits
|
|
403
|
+
const limitsPart = formatLimitsPart(
|
|
404
|
+
data.usageLimits?.five_hour ?? null,
|
|
405
|
+
data.periodCost ?? 0,
|
|
406
|
+
config.limits,
|
|
407
|
+
);
|
|
408
|
+
if (limitsPart) sections.push(limitsPart);
|
|
409
|
+
|
|
410
|
+
// Weekly
|
|
411
|
+
const weeklyPart = formatWeeklyPart(
|
|
412
|
+
data.usageLimits?.seven_day ?? null,
|
|
413
|
+
data.usageLimits?.five_hour?.utilization ?? null,
|
|
414
|
+
data.periodCost ?? 0,
|
|
415
|
+
config.weeklyUsage,
|
|
416
|
+
);
|
|
417
|
+
if (weeklyPart) sections.push(weeklyPart);
|
|
418
|
+
|
|
419
|
+
// Daily
|
|
420
|
+
const dailyPart = formatDailyPart(data.todayCost ?? 0, config.dailySpend);
|
|
421
|
+
if (dailyPart) sections.push(dailyPart);
|
|
422
|
+
|
|
155
423
|
const output = sections.join(` ${sep} `);
|
|
156
424
|
|
|
157
425
|
if (config.oneLine) return output;
|
|
@@ -163,9 +431,61 @@ export function renderStatusline(
|
|
|
163
431
|
}
|
|
164
432
|
|
|
165
433
|
// ─────────────────────────────────────────────────────────────
|
|
166
|
-
//
|
|
434
|
+
// LEGACY SUPPORT - For backwards compatibility with old data format
|
|
167
435
|
// ─────────────────────────────────────────────────────────────
|
|
168
436
|
|
|
437
|
+
export function renderStatusline(
|
|
438
|
+
data: StatuslineData,
|
|
439
|
+
config: StatuslineConfig,
|
|
440
|
+
): string {
|
|
441
|
+
// Convert legacy format to raw format
|
|
442
|
+
// Parse pre-formatted values back to raw (best effort)
|
|
443
|
+
const rawData: RawStatuslineData = {
|
|
444
|
+
git: parseGitFromBranch(data.branch),
|
|
445
|
+
path: data.dirPath.startsWith("~") ? data.dirPath : data.dirPath,
|
|
446
|
+
modelName: data.modelName,
|
|
447
|
+
cost: parseFloat(data.sessionCost.replace(/[$,]/g, "")) || 0,
|
|
448
|
+
durationMs: parseDurationToMs(data.sessionDuration),
|
|
449
|
+
contextTokens: data.contextTokens,
|
|
450
|
+
contextPercentage: data.contextPercentage,
|
|
451
|
+
usageLimits: data.usageLimits,
|
|
452
|
+
periodCost: data.periodCost,
|
|
453
|
+
todayCost: data.todayCost,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
return renderStatuslineRaw(rawData, config);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Helper to parse legacy branch string back to git data
|
|
460
|
+
function parseGitFromBranch(branch: string): RawGitData | null {
|
|
461
|
+
if (!branch) return null;
|
|
462
|
+
|
|
463
|
+
// Parse "main* +10 -5" format
|
|
464
|
+
const dirty = branch.includes("*");
|
|
465
|
+
const branchName =
|
|
466
|
+
branch.replace(/\*.*$/, "").replace(/\*/, "").trim() || "main";
|
|
467
|
+
|
|
468
|
+
const addMatch = branch.match(/\+(\d+)/);
|
|
469
|
+
const delMatch = branch.match(/-(\d+)/);
|
|
470
|
+
const added = addMatch ? parseInt(addMatch[1], 10) : 0;
|
|
471
|
+
const deleted = delMatch ? parseInt(delMatch[1], 10) : 0;
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
branch: branchName,
|
|
475
|
+
dirty,
|
|
476
|
+
staged: {
|
|
477
|
+
files: 0,
|
|
478
|
+
added: Math.floor(added / 2),
|
|
479
|
+
deleted: Math.floor(deleted / 2),
|
|
480
|
+
},
|
|
481
|
+
unstaged: {
|
|
482
|
+
files: 0,
|
|
483
|
+
added: Math.ceil(added / 2),
|
|
484
|
+
deleted: Math.ceil(deleted / 2),
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
169
489
|
// Helper to parse "12m" or "1h 30m" back to ms
|
|
170
490
|
function parseDurationToMs(duration: string): number {
|
|
171
491
|
let ms = 0;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function normalizeResetsAt(resetsAt: string): string {
|
|
2
|
+
try {
|
|
3
|
+
const date = new Date(resetsAt);
|
|
4
|
+
const minutes = date.getMinutes();
|
|
5
|
+
const roundedMinutes = Math.round(minutes / 5) * 5;
|
|
6
|
+
|
|
7
|
+
date.setMinutes(roundedMinutes);
|
|
8
|
+
date.setSeconds(0);
|
|
9
|
+
date.setMilliseconds(0);
|
|
10
|
+
|
|
11
|
+
return date.toISOString();
|
|
12
|
+
} catch {
|
|
13
|
+
return resetsAt;
|
|
14
|
+
}
|
|
15
|
+
}
|