@oh-my-pi/pi-utils 17.2.9 → 17.2.11

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 (84) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/types/acp/connection.d.ts +118 -0
  3. package/dist/types/acp/protocol.d.ts +526 -0
  4. package/dist/types/acp/schema.d.ts +41 -0
  5. package/dist/types/acp/stream.d.ts +8 -0
  6. package/dist/types/acp/transport.d.ts +81 -0
  7. package/dist/types/acp.d.ts +6 -0
  8. package/dist/types/browsers.d.ts +68 -0
  9. package/dist/types/chalk.d.ts +125 -0
  10. package/dist/types/dates.d.ts +7 -0
  11. package/dist/types/docx/converter.d.ts +46 -0
  12. package/dist/types/docx/xml.d.ts +26 -0
  13. package/dist/types/docx/zip.d.ts +6 -0
  14. package/dist/types/docx.d.ts +11 -0
  15. package/dist/types/dom/core.d.ts +431 -0
  16. package/dist/types/dom/parser.d.ts +7 -0
  17. package/dist/types/dom/selector.d.ts +5 -0
  18. package/dist/types/dom.d.ts +5 -0
  19. package/dist/types/frontmatter.d.ts +21 -0
  20. package/dist/types/headers.d.ts +34 -0
  21. package/dist/types/logger/rotating-file.d.ts +18 -0
  22. package/dist/types/lru.d.ts +46 -0
  23. package/dist/types/marked/core.d.ts +445 -0
  24. package/dist/types/marked.d.ts +2 -0
  25. package/dist/types/postmortem.d.ts +14 -0
  26. package/dist/types/procmgr.d.ts +16 -0
  27. package/dist/types/prompt.d.ts +2 -2
  28. package/dist/types/readability/readability.d.ts +9 -0
  29. package/dist/types/readability/readerable.d.ts +10 -0
  30. package/dist/types/readability/types.d.ts +70 -0
  31. package/dist/types/readability.d.ts +4 -0
  32. package/dist/types/template.d.ts +62 -0
  33. package/dist/types/turndown/gfm.d.ts +11 -0
  34. package/dist/types/turndown/html.d.ts +5 -0
  35. package/dist/types/turndown/service.d.ts +21 -0
  36. package/dist/types/turndown/types.d.ts +70 -0
  37. package/dist/types/turndown.d.ts +4 -0
  38. package/dist/types/vterm/buffer.d.ts +99 -0
  39. package/dist/types/vterm/terminal.d.ts +44 -0
  40. package/dist/types/vterm.d.ts +8 -0
  41. package/dist/types/xml.d.ts +31 -0
  42. package/package.json +2 -5
  43. package/src/acp/connection.ts +344 -0
  44. package/src/acp/protocol.ts +466 -0
  45. package/src/acp/schema.ts +160 -0
  46. package/src/acp/stream.ts +82 -0
  47. package/src/acp/transport.ts +213 -0
  48. package/src/acp.ts +6 -0
  49. package/src/browsers.ts +501 -0
  50. package/src/chalk.ts +312 -0
  51. package/src/dates.ts +194 -0
  52. package/src/docx/converter.ts +681 -0
  53. package/src/docx/xml.ts +166 -0
  54. package/src/docx/zip.ts +87 -0
  55. package/src/docx.ts +20 -0
  56. package/src/dom/core.ts +1254 -0
  57. package/src/dom/parser.ts +370 -0
  58. package/src/dom/selector.ts +290 -0
  59. package/src/dom.ts +33 -0
  60. package/src/frontmatter.ts +44 -14
  61. package/src/headers.ts +167 -0
  62. package/src/logger/rotating-file.ts +149 -0
  63. package/src/logger.ts +12 -28
  64. package/src/lru.ts +185 -0
  65. package/src/marked/core.ts +1576 -0
  66. package/src/marked.ts +2 -0
  67. package/src/postmortem.ts +44 -1
  68. package/src/procmgr.ts +21 -4
  69. package/src/prompt.ts +5 -22
  70. package/src/readability/readability.ts +533 -0
  71. package/src/readability/readerable.ts +51 -0
  72. package/src/readability/types.ts +72 -0
  73. package/src/readability.ts +11 -0
  74. package/src/template.ts +586 -0
  75. package/src/turndown/gfm.ts +106 -0
  76. package/src/turndown/html.ts +257 -0
  77. package/src/turndown/service.ts +334 -0
  78. package/src/turndown/types.ts +81 -0
  79. package/src/turndown.ts +5 -0
  80. package/src/vterm/buffer.ts +218 -0
  81. package/src/vterm/terminal.ts +773 -0
  82. package/src/vterm.ts +8 -0
  83. package/src/xml.ts +313 -0
  84. package/src/winston-daily-rotate-file.d.ts +0 -6
package/src/chalk.ts ADDED
@@ -0,0 +1,312 @@
1
+ /** Behavior-compatible reimplementation of chalk's used surface. */
2
+
3
+ import { hexToRgb } from "./color";
4
+
5
+ /** ANSI color capability level. */
6
+ export type ColorLevel = 0 | 1 | 2 | 3;
7
+
8
+ /** Details about a terminal's supported color depth. */
9
+ export interface ColorSupport {
10
+ /** Highest supported ANSI color level. */
11
+ level: ColorLevel;
12
+ /** Whether the terminal supports the basic 16 colors. */
13
+ hasBasic: boolean;
14
+ /** Whether the terminal supports the 256-color palette. */
15
+ has256: boolean;
16
+ /** Whether the terminal supports 24-bit color. */
17
+ has16m: boolean;
18
+ }
19
+
20
+ interface ChalkOptions {
21
+ level?: ColorLevel;
22
+ }
23
+
24
+ interface Style {
25
+ open: string;
26
+ close: string;
27
+ }
28
+
29
+ interface ChalkContext {
30
+ level: number;
31
+ }
32
+
33
+ /** A callable, chainable ANSI text formatter. */
34
+ export interface ChalkInstance {
35
+ (...text: unknown[]): string;
36
+ /** Active color capability level. */
37
+ level: ColorLevel;
38
+ /** Reset every active terminal style. */
39
+ readonly reset: ChalkInstance;
40
+ /** Render bold text. */
41
+ readonly bold: ChalkInstance;
42
+ /** Render faint text. */
43
+ readonly dim: ChalkInstance;
44
+ /** Render italic text. */
45
+ readonly italic: ChalkInstance;
46
+ /** Render underlined text. */
47
+ readonly underline: ChalkInstance;
48
+ /** Swap foreground and background colors. */
49
+ readonly inverse: ChalkInstance;
50
+ /** Render struck-through text. */
51
+ readonly strikethrough: ChalkInstance;
52
+ /** Render black text. */
53
+ readonly black: ChalkInstance;
54
+ /** Render red text. */
55
+ readonly red: ChalkInstance;
56
+ /** Render green text. */
57
+ readonly green: ChalkInstance;
58
+ /** Render yellow text. */
59
+ readonly yellow: ChalkInstance;
60
+ /** Render blue text. */
61
+ readonly blue: ChalkInstance;
62
+ /** Render magenta text. */
63
+ readonly magenta: ChalkInstance;
64
+ /** Render cyan text. */
65
+ readonly cyan: ChalkInstance;
66
+ /** Render white text. */
67
+ readonly white: ChalkInstance;
68
+ /** Render gray text. */
69
+ readonly gray: ChalkInstance;
70
+ /** Alias for gray text. */
71
+ readonly grey: ChalkInstance;
72
+ /** Render bright black text. */
73
+ readonly blackBright: ChalkInstance;
74
+ /** Render bright red text. */
75
+ readonly redBright: ChalkInstance;
76
+ /** Render bright green text. */
77
+ readonly greenBright: ChalkInstance;
78
+ /** Render bright yellow text. */
79
+ readonly yellowBright: ChalkInstance;
80
+ /** Render bright blue text. */
81
+ readonly blueBright: ChalkInstance;
82
+ /** Render bright magenta text. */
83
+ readonly magentaBright: ChalkInstance;
84
+ /** Render bright cyan text. */
85
+ readonly cyanBright: ChalkInstance;
86
+ /** Render bright white text. */
87
+ readonly whiteBright: ChalkInstance;
88
+ /** Render text on a black background. */
89
+ readonly bgBlack: ChalkInstance;
90
+ /** Render text on a red background. */
91
+ readonly bgRed: ChalkInstance;
92
+ /** Render text on a green background. */
93
+ readonly bgGreen: ChalkInstance;
94
+ /** Render text on a yellow background. */
95
+ readonly bgYellow: ChalkInstance;
96
+ /** Render text on a blue background. */
97
+ readonly bgBlue: ChalkInstance;
98
+ /** Render text on a magenta background. */
99
+ readonly bgMagenta: ChalkInstance;
100
+ /** Render text on a cyan background. */
101
+ readonly bgCyan: ChalkInstance;
102
+ /** Render text on a white background. */
103
+ readonly bgWhite: ChalkInstance;
104
+ /** Render text on a gray background. */
105
+ readonly bgGray: ChalkInstance;
106
+ /** Alias for a gray background. */
107
+ readonly bgGrey: ChalkInstance;
108
+ /** Render text on a bright black background. */
109
+ readonly bgBlackBright: ChalkInstance;
110
+ /** Render text on a bright red background. */
111
+ readonly bgRedBright: ChalkInstance;
112
+ /** Render text on a bright green background. */
113
+ readonly bgGreenBright: ChalkInstance;
114
+ /** Render text on a bright yellow background. */
115
+ readonly bgYellowBright: ChalkInstance;
116
+ /** Render text on a bright blue background. */
117
+ readonly bgBlueBright: ChalkInstance;
118
+ /** Render text on a bright magenta background. */
119
+ readonly bgMagentaBright: ChalkInstance;
120
+ /** Render text on a bright cyan background. */
121
+ readonly bgCyanBright: ChalkInstance;
122
+ /** Render text on a bright white background. */
123
+ readonly bgWhiteBright: ChalkInstance;
124
+ /** Render text with an arbitrary hexadecimal foreground color. */
125
+ hex(color: string): ChalkInstance;
126
+ }
127
+
128
+ /** Constructor for an independently configured chalk formatter. */
129
+ export interface ChalkConstructor {
130
+ new (options?: ChalkOptions): ChalkInstance;
131
+ }
132
+
133
+ const ESC = "\u001B[";
134
+ const STYLES: Readonly<Record<string, Style>> = {
135
+ reset: { open: `${ESC}0m`, close: `${ESC}0m` },
136
+ bold: { open: `${ESC}1m`, close: `${ESC}22m` },
137
+ dim: { open: `${ESC}2m`, close: `${ESC}22m` },
138
+ italic: { open: `${ESC}3m`, close: `${ESC}23m` },
139
+ underline: { open: `${ESC}4m`, close: `${ESC}24m` },
140
+ inverse: { open: `${ESC}7m`, close: `${ESC}27m` },
141
+ strikethrough: { open: `${ESC}9m`, close: `${ESC}29m` },
142
+ black: { open: `${ESC}30m`, close: `${ESC}39m` },
143
+ red: { open: `${ESC}31m`, close: `${ESC}39m` },
144
+ green: { open: `${ESC}32m`, close: `${ESC}39m` },
145
+ yellow: { open: `${ESC}33m`, close: `${ESC}39m` },
146
+ blue: { open: `${ESC}34m`, close: `${ESC}39m` },
147
+ magenta: { open: `${ESC}35m`, close: `${ESC}39m` },
148
+ cyan: { open: `${ESC}36m`, close: `${ESC}39m` },
149
+ white: { open: `${ESC}37m`, close: `${ESC}39m` },
150
+ gray: { open: `${ESC}90m`, close: `${ESC}39m` },
151
+ blackBright: { open: `${ESC}90m`, close: `${ESC}39m` },
152
+ redBright: { open: `${ESC}91m`, close: `${ESC}39m` },
153
+ greenBright: { open: `${ESC}92m`, close: `${ESC}39m` },
154
+ yellowBright: { open: `${ESC}93m`, close: `${ESC}39m` },
155
+ blueBright: { open: `${ESC}94m`, close: `${ESC}39m` },
156
+ magentaBright: { open: `${ESC}95m`, close: `${ESC}39m` },
157
+ cyanBright: { open: `${ESC}96m`, close: `${ESC}39m` },
158
+ whiteBright: { open: `${ESC}97m`, close: `${ESC}39m` },
159
+ bgBlack: { open: `${ESC}40m`, close: `${ESC}49m` },
160
+ bgRed: { open: `${ESC}41m`, close: `${ESC}49m` },
161
+ bgGreen: { open: `${ESC}42m`, close: `${ESC}49m` },
162
+ bgYellow: { open: `${ESC}43m`, close: `${ESC}49m` },
163
+ bgBlue: { open: `${ESC}44m`, close: `${ESC}49m` },
164
+ bgMagenta: { open: `${ESC}45m`, close: `${ESC}49m` },
165
+ bgCyan: { open: `${ESC}46m`, close: `${ESC}49m` },
166
+ bgWhite: { open: `${ESC}47m`, close: `${ESC}49m` },
167
+ bgBlackBright: { open: `${ESC}100m`, close: `${ESC}49m` },
168
+ bgRedBright: { open: `${ESC}101m`, close: `${ESC}49m` },
169
+ bgGreenBright: { open: `${ESC}102m`, close: `${ESC}49m` },
170
+ bgYellowBright: { open: `${ESC}103m`, close: `${ESC}49m` },
171
+ bgBlueBright: { open: `${ESC}104m`, close: `${ESC}49m` },
172
+ bgMagentaBright: { open: `${ESC}105m`, close: `${ESC}49m` },
173
+ bgCyanBright: { open: `${ESC}106m`, close: `${ESC}49m` },
174
+ bgWhiteBright: { open: `${ESC}107m`, close: `${ESC}49m` },
175
+ };
176
+
177
+ function parseForcedLevel(value: string | undefined): ColorLevel | undefined {
178
+ if (value === undefined) return undefined;
179
+ if (value === "" || value === "true") return 1;
180
+ if (value === "false") return 0;
181
+ const parsed = Number.parseInt(value, 10);
182
+ if (Number.isNaN(parsed)) return 1;
183
+ return Math.max(0, Math.min(3, parsed)) as ColorLevel;
184
+ }
185
+
186
+ /** Detect an ANSI color level from terminal environment and TTY state. */
187
+ export function detectColorLevel(environment: NodeJS.ProcessEnv, isTTY: boolean): ColorLevel {
188
+ const forced = parseForcedLevel(environment.FORCE_COLOR);
189
+ if (forced !== undefined) return forced;
190
+ if ("NO_COLOR" in environment || environment.TERM === "dumb") return 0;
191
+ if (!isTTY) return 0;
192
+ if (environment.COLORTERM === "truecolor" || environment.COLORTERM === "24bit") return 3;
193
+ if (environment.TERM?.endsWith("-256color")) return 2;
194
+ if (environment.CI) {
195
+ if (environment.GITHUB_ACTIONS || environment.GITEA_ACTIONS) return 3;
196
+ if (
197
+ environment.TRAVIS ||
198
+ environment.CIRCLECI ||
199
+ environment.APPVEYOR ||
200
+ environment.GITLAB_CI ||
201
+ environment.BUILDKITE
202
+ ) {
203
+ return 1;
204
+ }
205
+ return 0;
206
+ }
207
+ if (environment.TERM_PROGRAM === "iTerm.app") {
208
+ return Number.parseInt(environment.TERM_PROGRAM_VERSION ?? "0", 10) >= 3 ? 3 : 2;
209
+ }
210
+ if (environment.TERM_PROGRAM === "Apple_Terminal") return 2;
211
+ return 1;
212
+ }
213
+
214
+ function colorSupport(level: ColorLevel): ColorSupport | false {
215
+ if (level === 0) return false;
216
+ return { level, hasBasic: true, has256: level >= 2, has16m: level >= 3 };
217
+ }
218
+
219
+ /** Color support detected for standard output. */
220
+ export const supportsColor = colorSupport(detectColorLevel(process.env, Boolean(process.stdout.isTTY)));
221
+
222
+ /** Color support detected for standard error. */
223
+ export const supportsColorStderr = colorSupport(detectColorLevel(process.env, Boolean(process.stderr.isTTY)));
224
+
225
+ function ansi256(r: number, g: number, b: number): number {
226
+ if (r === g && g === b) {
227
+ if (r < 8) return 16;
228
+ if (r > 248) return 231;
229
+ return Math.round(((r - 8) / 247) * 24) + 232;
230
+ }
231
+ return 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
232
+ }
233
+
234
+ function ansi16(r: number, g: number, b: number): number {
235
+ const value = Math.max(r, g, b);
236
+ if (value < 50) return 30;
237
+ let code = 30 + ((Math.round(b / 255) << 2) | (Math.round(g / 255) << 1) | Math.round(r / 255));
238
+ if (value > 191) code += 60;
239
+ return code;
240
+ }
241
+
242
+ function hexStyle(color: string, level: number): Style {
243
+ const { r, g, b } = hexToRgb(color);
244
+ const open =
245
+ level >= 3
246
+ ? `${ESC}38;2;${r};${g};${b}m`
247
+ : level === 2
248
+ ? `${ESC}38;5;${ansi256(r, g, b)}m`
249
+ : `${ESC}${ansi16(r, g, b)}m`;
250
+ return { open, close: `${ESC}39m` };
251
+ }
252
+
253
+ function applyStyles(input: string, styles: readonly Style[]): string {
254
+ let output = input;
255
+ let opening = "";
256
+ let closing = "";
257
+ for (let index = styles.length - 1; index >= 0; index--) {
258
+ const style = styles[index];
259
+ if (!style) continue;
260
+ output = output.split(style.close).join(style.close + style.open);
261
+ opening = style.open + opening;
262
+ closing += style.close;
263
+ }
264
+ output = output.replace(/\r?\n/g, match => `${closing}${match}${opening}`);
265
+ return opening + output + closing;
266
+ }
267
+
268
+ function createBuilder(context: ChalkContext, styles: readonly Style[]): ChalkInstance {
269
+ const builder = ((...values: unknown[]) => {
270
+ let text = "";
271
+ for (let index = 0; index < values.length; index++) {
272
+ if (index > 0) text += " ";
273
+ text += String(values[index]);
274
+ }
275
+ if (text.length === 0 || context.level === 0) return text;
276
+ return applyStyles(text, styles);
277
+ }) as ChalkInstance;
278
+ Object.defineProperty(builder, "level", {
279
+ get: () => context.level,
280
+ set: (level: number) => {
281
+ context.level = level;
282
+ },
283
+ });
284
+ for (const name in STYLES) {
285
+ const style = STYLES[name];
286
+ if (style) Object.defineProperty(builder, name, { get: () => createBuilder(context, [...styles, style]) });
287
+ }
288
+ Object.defineProperty(builder, "grey", { get: () => createBuilder(context, [...styles, STYLES.gray!]) });
289
+ Object.defineProperty(builder, "bgGray", {
290
+ get: () => createBuilder(context, [...styles, STYLES.bgBlackBright!]),
291
+ });
292
+ Object.defineProperty(builder, "bgGrey", {
293
+ get: () => createBuilder(context, [...styles, STYLES.bgBlackBright!]),
294
+ });
295
+ Object.defineProperty(builder, "hex", {
296
+ value: (color: string) => createBuilder(context, [...styles, hexStyle(color, context.level)]),
297
+ });
298
+ return builder;
299
+ }
300
+
301
+ function ChalkImplementation(this: unknown, options: ChalkOptions = {}): ChalkInstance {
302
+ const detectedLevel = supportsColor === false ? 0 : supportsColor.level;
303
+ return createBuilder({ level: options.level ?? detectedLevel }, []);
304
+ }
305
+
306
+ /** Construct an independently configured chalk formatter. */
307
+ export const Chalk = ChalkImplementation as unknown as ChalkConstructor;
308
+
309
+ const defaultLevel = supportsColor === false ? 0 : supportsColor.level;
310
+ const chalk = new Chalk({ level: defaultLevel });
311
+
312
+ export default chalk;
package/src/dates.ts ADDED
@@ -0,0 +1,194 @@
1
+ /** Behavior-compatible reimplementation of date-fns's used surface. */
2
+
3
+ const MONTHS = [
4
+ "January",
5
+ "February",
6
+ "March",
7
+ "April",
8
+ "May",
9
+ "June",
10
+ "July",
11
+ "August",
12
+ "September",
13
+ "October",
14
+ "November",
15
+ "December",
16
+ ] as const;
17
+ const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] as const;
18
+ const TOKENS = [
19
+ "MMMM",
20
+ "yyyy",
21
+ "EEEE",
22
+ "MMM",
23
+ "EEE",
24
+ "yy",
25
+ "MM",
26
+ "dd",
27
+ "HH",
28
+ "hh",
29
+ "mm",
30
+ "ss",
31
+ "M",
32
+ "d",
33
+ "H",
34
+ "h",
35
+ "m",
36
+ "s",
37
+ "a",
38
+ ] as const;
39
+
40
+ function pad(value: number, length = 2): string {
41
+ return String(value).padStart(length, "0");
42
+ }
43
+
44
+ function tokenValue(date: Date, token: (typeof TOKENS)[number]): string {
45
+ const month = date.getMonth();
46
+ const weekday = date.getDay();
47
+ const hours = date.getHours();
48
+ const hours12 = hours % 12 || 12;
49
+
50
+ switch (token) {
51
+ case "yyyy":
52
+ return pad(date.getFullYear(), 4);
53
+ case "yy":
54
+ return pad(date.getFullYear() % 100);
55
+ case "MMMM":
56
+ return MONTHS[month];
57
+ case "MMM":
58
+ return MONTHS[month].slice(0, 3);
59
+ case "MM":
60
+ return pad(month + 1);
61
+ case "M":
62
+ return String(month + 1);
63
+ case "dd":
64
+ return pad(date.getDate());
65
+ case "d":
66
+ return String(date.getDate());
67
+ case "EEEE":
68
+ return WEEKDAYS[weekday];
69
+ case "EEE":
70
+ return WEEKDAYS[weekday].slice(0, 3);
71
+ case "HH":
72
+ return pad(hours);
73
+ case "H":
74
+ return String(hours);
75
+ case "hh":
76
+ return pad(hours12);
77
+ case "h":
78
+ return String(hours12);
79
+ case "mm":
80
+ return pad(date.getMinutes());
81
+ case "m":
82
+ return String(date.getMinutes());
83
+ case "ss":
84
+ return pad(date.getSeconds());
85
+ case "s":
86
+ return String(date.getSeconds());
87
+ case "a":
88
+ return hours < 12 ? "AM" : "PM";
89
+ }
90
+ }
91
+
92
+ function asDate(value: Date | number): Date {
93
+ const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
94
+ if (Number.isNaN(date.getTime())) throw new RangeError("Invalid time value");
95
+ return date;
96
+ }
97
+
98
+ /** Format a date with the supported date-fns v4 tokens and quoted literals. */
99
+ export function format(value: Date | number, pattern: string): string {
100
+ const date = asDate(value);
101
+ let result = "";
102
+
103
+ for (let index = 0; index < pattern.length; ) {
104
+ if (pattern[index] === "'") {
105
+ if (pattern[index + 1] === "'") {
106
+ result += "'";
107
+ index += 2;
108
+ continue;
109
+ }
110
+
111
+ index++;
112
+ while (index < pattern.length) {
113
+ if (pattern[index] !== "'") {
114
+ result += pattern[index++];
115
+ continue;
116
+ }
117
+ if (pattern[index + 1] === "'") {
118
+ result += "'";
119
+ index += 2;
120
+ continue;
121
+ }
122
+ index++;
123
+ break;
124
+ }
125
+ continue;
126
+ }
127
+
128
+ const rest = pattern.slice(index);
129
+ const token = TOKENS.find(candidate => rest.startsWith(candidate));
130
+ if (token) {
131
+ result += tokenValue(date, token);
132
+ index += token.length;
133
+ continue;
134
+ }
135
+
136
+ const character = pattern[index++];
137
+ if (/[A-Za-z]/.test(character)) {
138
+ throw new RangeError(`Format string contains an unescaped latin alphabet character \`${character}\``);
139
+ }
140
+ result += character;
141
+ }
142
+
143
+ return result;
144
+ }
145
+
146
+ function plural(count: number, singular: string): string {
147
+ return `${count} ${count === 1 ? singular : `${singular}s`}`;
148
+ }
149
+
150
+ function completedMonths(earlier: Date, later: Date): number {
151
+ let months = (later.getFullYear() - earlier.getFullYear()) * 12 + later.getMonth() - earlier.getMonth();
152
+ if (months === 0) return 0;
153
+
154
+ const candidate = new Date(earlier);
155
+ candidate.setDate(1);
156
+ candidate.setFullYear(earlier.getFullYear(), earlier.getMonth() + months, 1);
157
+ const lastDay = new Date(candidate.getFullYear(), candidate.getMonth() + 1, 0).getDate();
158
+ candidate.setDate(Math.min(earlier.getDate(), lastDay));
159
+ if (candidate.getTime() > later.getTime()) months--;
160
+ return months;
161
+ }
162
+
163
+ function distanceWords(earlier: Date, later: Date): string {
164
+ const seconds = (later.getTime() - earlier.getTime()) / 1_000;
165
+ const timezoneOffset = (later.getTimezoneOffset() - earlier.getTimezoneOffset()) * 60;
166
+ const minutes = Math.round((seconds - timezoneOffset) / 60);
167
+
168
+ if (minutes < 2) return minutes === 0 ? "less than a minute" : "1 minute";
169
+ if (minutes < 45) return plural(minutes, "minute");
170
+ if (minutes < 90) return "about 1 hour";
171
+ if (minutes < 1_440) return `about ${plural(Math.round(minutes / 60), "hour")}`;
172
+ if (minutes < 2_520) return "1 day";
173
+ if (minutes < 43_200) return plural(Math.round(minutes / 1_440), "day");
174
+ if (minutes < 86_400) return `about ${plural(Math.round(minutes / 43_200), "month")}`;
175
+
176
+ const months = completedMonths(earlier, later);
177
+ if (months < 12) return plural(Math.round(minutes / 43_200), "month");
178
+
179
+ const years = Math.trunc(months / 12);
180
+ const remainingMonths = months % 12;
181
+ if (remainingMonths < 3) return `about ${plural(years, "year")}`;
182
+ if (remainingMonths < 9) return `over ${plural(years, "year")}`;
183
+ return `almost ${plural(years + 1, "year")}`;
184
+ }
185
+
186
+ /** Describe the distance from a date to now using date-fns's English thresholds. */
187
+ export function formatDistanceToNow(value: Date | number, options: { addSuffix?: boolean } = {}): string {
188
+ const date = asDate(value);
189
+ const now = new Date(Date.now());
190
+ const isFuture = date.getTime() > now.getTime();
191
+ const words = isFuture ? distanceWords(now, date) : distanceWords(date, now);
192
+ if (!options.addSuffix) return words;
193
+ return isFuture ? `in ${words}` : `${words} ago`;
194
+ }