@oh-my-pi/pi-utils 17.2.8 → 17.2.9
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/CHANGELOG.md +11 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/version.d.ts +18 -0
- package/package.json +2 -2
- package/src/fetch-retry.ts +7 -1
- package/src/frontmatter.ts +1 -1
- package/src/index.ts +1 -0
- package/src/version.ts +99 -0
- package/src/which.ts +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.9] - 2026-08-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added a public `compareVersions` utility (`@oh-my-pi/pi-utils`) that compares two version strings with SemVer-2.0 prerelease ordering, build-metadata stripping, and numeric segment comparison without float overflow; never throws.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Honor the current process `PATH` when caching executable lookups, preventing stale tool paths after environment reloads.
|
|
14
|
+
- Parsed account-cap reset windows such as “Your limit will reset in 13 minutes” so credential backoff honors the provider's full reset duration.
|
|
15
|
+
|
|
5
16
|
## [17.2.6] - 2026-08-03
|
|
6
17
|
|
|
7
18
|
### Added
|
package/dist/types/index.d.ts
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two version strings.
|
|
3
|
+
*
|
|
4
|
+
* Canonical comparator that supersedes the historical in-repo copies
|
|
5
|
+
* (update-cli, hackage scraper, release scripts):
|
|
6
|
+
* - inputs are trimmed and at most one leading `v`/`V` is stripped
|
|
7
|
+
* - dot-separated segments are compared numerically, missing trailing
|
|
8
|
+
* segments count as 0, so `1.2` === `1.2.0` and any segment count works
|
|
9
|
+
* - a SemVer-2.0 prerelease suffix sorts before the plain release
|
|
10
|
+
* (`1.0.0-beta` < `1.0.0`); prerelease identifiers follow SemVer order
|
|
11
|
+
* (numeric < alphanumeric, numeric compared by value, alphanumeric
|
|
12
|
+
* compared lexically, longer sets of equal fields win)
|
|
13
|
+
* - SemVer build metadata begins at the first `+` and does not participate
|
|
14
|
+
* in precedence; it is stripped before core/prerelease parsing
|
|
15
|
+
* - malformed numeric segments compare as 0 (`1.2.x` === `1.2.0`)
|
|
16
|
+
* - never throws; returns only -1 | 0 | 1
|
|
17
|
+
*/
|
|
18
|
+
export declare function compareVersions(a: string, b: string): number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "17.2.
|
|
4
|
+
"version": "17.2.9",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "17.2.
|
|
34
|
+
"@oh-my-pi/pi-natives": "17.2.9",
|
|
35
35
|
"handlebars": "^4.7.9",
|
|
36
36
|
"winston": "^3.19.0",
|
|
37
37
|
"winston-daily-rotate-file": "5.0.0"
|
package/src/fetch-retry.ts
CHANGED
|
@@ -10,6 +10,8 @@ const RETRY_DELAY_FIELD_PATTERN = /"retryDelay":\s*"([0-9.]+)(ms|s)"/i;
|
|
|
10
10
|
// "try again in 5 min" / "try again in ~158 min." / "try again in 2h" /
|
|
11
11
|
// "try again in 90 minutes" / "try again in 1 hour"
|
|
12
12
|
const TRY_AGAIN_PATTERN = /try again in\s+~?\s*([0-9.]+)\s*(ms|sec|s|minutes?|mins?|m|hours?|hrs?|h)\b/i;
|
|
13
|
+
// "Your limit will reset in 13 minutes" / "reset in 13 minutes" / "will reset in 2h"
|
|
14
|
+
const WILL_RESET_IN_PATTERN = /(?:will\s+)?reset in\s+~?\s*([0-9.]+)\s*(ms|sec|s|minutes?|mins?|m|hours?|hrs?|h)\b/i;
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Server-suggested retry delay extraction. Merges the patterns historically used
|
|
@@ -83,7 +85,11 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
83
85
|
if (totalMs > 0) return totalMs;
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
|
-
|
|
88
|
+
// Account-reset hints ("will reset in …") take precedence over short
|
|
89
|
+
// retry hints ("please retry in 5s"): a body carrying both must honour the
|
|
90
|
+
// longer account window, not the shorter generic one. QUOTA_RESET_PATTERN
|
|
91
|
+
// ("reset after …") above already runs first and stays first.
|
|
92
|
+
for (const pattern of [WILL_RESET_IN_PATTERN, PLEASE_RETRY_PATTERN, RETRY_DELAY_FIELD_PATTERN, TRY_AGAIN_PATTERN]) {
|
|
87
93
|
const match = pattern.exec(body);
|
|
88
94
|
if (match?.[1]) {
|
|
89
95
|
const value = Number.parseFloat(match[1]);
|
package/src/frontmatter.ts
CHANGED
|
@@ -71,7 +71,7 @@ export class FrontmatterError extends Error {
|
|
|
71
71
|
this.name = "FrontmatterError";
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
toString(): string {
|
|
74
|
+
override toString(): string {
|
|
75
75
|
// Format the error with stack and detail, including the error message, stack, and source if present
|
|
76
76
|
const details: string[] = [this.message];
|
|
77
77
|
if (this.source !== undefined) {
|
package/src/index.ts
CHANGED
package/src/version.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const DIGITS = /^\d+$/;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compare two version strings.
|
|
5
|
+
*
|
|
6
|
+
* Canonical comparator that supersedes the historical in-repo copies
|
|
7
|
+
* (update-cli, hackage scraper, release scripts):
|
|
8
|
+
* - inputs are trimmed and at most one leading `v`/`V` is stripped
|
|
9
|
+
* - dot-separated segments are compared numerically, missing trailing
|
|
10
|
+
* segments count as 0, so `1.2` === `1.2.0` and any segment count works
|
|
11
|
+
* - a SemVer-2.0 prerelease suffix sorts before the plain release
|
|
12
|
+
* (`1.0.0-beta` < `1.0.0`); prerelease identifiers follow SemVer order
|
|
13
|
+
* (numeric < alphanumeric, numeric compared by value, alphanumeric
|
|
14
|
+
* compared lexically, longer sets of equal fields win)
|
|
15
|
+
* - SemVer build metadata begins at the first `+` and does not participate
|
|
16
|
+
* in precedence; it is stripped before core/prerelease parsing
|
|
17
|
+
* - malformed numeric segments compare as 0 (`1.2.x` === `1.2.0`)
|
|
18
|
+
* - never throws; returns only -1 | 0 | 1
|
|
19
|
+
*/
|
|
20
|
+
export function compareVersions(a: string, b: string): number {
|
|
21
|
+
const pa = parseVersion(a);
|
|
22
|
+
const pb = parseVersion(b);
|
|
23
|
+
|
|
24
|
+
const core = compareNumericParts(pa.core, pb.core);
|
|
25
|
+
if (core !== 0) return core;
|
|
26
|
+
|
|
27
|
+
return comparePrerelease(pa.prerelease, pb.prerelease);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ParsedVersion {
|
|
31
|
+
core: string[];
|
|
32
|
+
prerelease: string[] | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseVersion(version: string): ParsedVersion {
|
|
36
|
+
const trimmed = version.trim();
|
|
37
|
+
const stripped = trimmed.startsWith("v") || trimmed.startsWith("V") ? trimmed.slice(1) : trimmed;
|
|
38
|
+
const plusIndex = stripped.indexOf("+");
|
|
39
|
+
const withoutBuild = plusIndex === -1 ? stripped : stripped.slice(0, plusIndex);
|
|
40
|
+
const dashIndex = withoutBuild.indexOf("-");
|
|
41
|
+
if (dashIndex === -1) {
|
|
42
|
+
return { core: withoutBuild.split("."), prerelease: null };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
core: withoutBuild.slice(0, dashIndex).split("."),
|
|
46
|
+
prerelease: withoutBuild.slice(dashIndex + 1).split("."),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Compare dot-separated numeric segments; missing/malformed segments count as 0. */
|
|
51
|
+
function compareNumericParts(a: string[], b: string[]): number {
|
|
52
|
+
const length = Math.max(a.length, b.length);
|
|
53
|
+
for (let i = 0; i < length; i++) {
|
|
54
|
+
// Missing or malformed segments compare as 0.
|
|
55
|
+
const sa = a[i];
|
|
56
|
+
const sb = b[i];
|
|
57
|
+
const result = compareDigits(
|
|
58
|
+
sa !== undefined && DIGITS.test(sa) ? sa : "0",
|
|
59
|
+
sb !== undefined && DIGITS.test(sb) ? sb : "0",
|
|
60
|
+
);
|
|
61
|
+
if (result !== 0) return result;
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Exact integer comparison of digit strings, avoiding float overflow. */
|
|
67
|
+
function compareDigits(a: string, b: string): number {
|
|
68
|
+
const na = a.replace(/^0+/, "") || "0";
|
|
69
|
+
const nb = b.replace(/^0+/, "") || "0";
|
|
70
|
+
if (na.length !== nb.length) return na.length < nb.length ? -1 : 1;
|
|
71
|
+
if (na < nb) return -1;
|
|
72
|
+
if (na > nb) return 1;
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** SemVer-2.0 prerelease ordering; null means a plain release, which wins. */
|
|
77
|
+
function comparePrerelease(a: string[] | null, b: string[] | null): number {
|
|
78
|
+
if (a === null || b === null) {
|
|
79
|
+
return a === b ? 0 : a === null ? 1 : -1;
|
|
80
|
+
}
|
|
81
|
+
const length = Math.max(a.length, b.length);
|
|
82
|
+
for (let i = 0; i < length; i++) {
|
|
83
|
+
const ia = a[i];
|
|
84
|
+
const ib = b[i];
|
|
85
|
+
if (ia === undefined) return -1;
|
|
86
|
+
if (ib === undefined) return 1;
|
|
87
|
+
const aNumeric = DIGITS.test(ia);
|
|
88
|
+
const bNumeric = DIGITS.test(ib);
|
|
89
|
+
if (aNumeric && bNumeric) {
|
|
90
|
+
const result = compareDigits(ia, ib);
|
|
91
|
+
if (result !== 0) return result;
|
|
92
|
+
} else if (aNumeric !== bNumeric) {
|
|
93
|
+
return aNumeric ? -1 : 1;
|
|
94
|
+
} else if (ia !== ib) {
|
|
95
|
+
return ia < ib ? -1 : 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
package/src/which.ts
CHANGED
|
@@ -183,8 +183,8 @@ export interface WhichOptions extends Bun.WhichOptions {
|
|
|
183
183
|
|
|
184
184
|
// Darwin-specific "which" shim: consult Xcode/CLT toolchain directories after $PATH.
|
|
185
185
|
// Uses cached directory listings instead of per-command existsSync or xcrun subprocesses.
|
|
186
|
-
function darwinWhich(command: string,
|
|
187
|
-
const regular = Bun.which(command);
|
|
186
|
+
function darwinWhich(command: string, options?: Bun.WhichOptions): string | null {
|
|
187
|
+
const regular = Bun.which(command, options);
|
|
188
188
|
if (regular) return regular;
|
|
189
189
|
if (isXcodeBin(command)) {
|
|
190
190
|
return getMacosToolPaths().get(command) ?? null;
|
|
@@ -214,17 +214,19 @@ function cacheKey(command: string, options?: Bun.WhichOptions): CacheKey {
|
|
|
214
214
|
*/
|
|
215
215
|
export function $which(command: string, options?: WhichOptions): string | null {
|
|
216
216
|
const cachePolicy = options?.cache ?? WhichCachePolicy.Cached;
|
|
217
|
+
const lookupOptions =
|
|
218
|
+
options?.PATH !== undefined || process.env.PATH === undefined ? options : { ...options, PATH: process.env.PATH };
|
|
217
219
|
let key: CacheKey | undefined;
|
|
218
220
|
|
|
219
221
|
if (cachePolicy !== WhichCachePolicy.Bypass) {
|
|
220
|
-
key = cacheKey(command,
|
|
222
|
+
key = cacheKey(command, lookupOptions);
|
|
221
223
|
if (cachePolicy !== WhichCachePolicy.Fresh) {
|
|
222
224
|
const cached = toolCache.get(key);
|
|
223
225
|
if (cached !== undefined) return cached;
|
|
224
226
|
}
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
const result = whichFresh(command,
|
|
229
|
+
const result = whichFresh(command, lookupOptions);
|
|
228
230
|
if (key != null && cachePolicy !== WhichCachePolicy.ReadOnly) {
|
|
229
231
|
toolCache.set(key, result);
|
|
230
232
|
}
|