Files
orca/src/shared/app-version.ts
T
OrcaWinandOrcaWin 0a5f9e9dc5 fix: keep identifier casing locale-independent (Turkish) (#34709)
* fix: keep identifier casing locale-independent (Turkish)

Under a tr/az default locale, toLocaleLowerCase()/toLocaleUpperCase() map
I to dotless ı and İ, and default-locale collation stops treating I/i as
base-equal. Switch identifier matching (Linear CLI selectors, priorities,
error classification, plugin/Jira/worktree/address/slash-command search) to
toLowerCase()/toUpperCase(), and pin collation to 'en' where ordering feeds
hashes, canonical JSON, semver precedence, remote selection, SSH Include
order, or WSL distro equality. User-visible prose casing and display-list
sorting stay locale-aware.

* fix: order SSH Include globs bytewise and fold dashboard search ids locale-independently

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 22:47:01 -07:00

115 lines
3.2 KiB
TypeScript

type ParsedVersion = {
core: [number, number, number]
prerelease: string[]
}
function parseVersion(value: string): ParsedVersion | null {
const normalized = value.trim().replace(/^v/i, '')
const match = normalized.match(
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+([0-9A-Za-z-.]+))?$/
)
if (!match) {
return null
}
return {
core: [Number(match[1]), Number(match[2]), Number(match[3])],
prerelease: match[4]?.split('.') ?? []
}
}
export function isValidAppVersion(value: string): boolean {
return parseVersion(value) !== null
}
/** Extracts the first valid semver (with prerelease/build) from CLI `--version` output. */
export function parseCliVersion(output: string | null | undefined): string | null {
const version = output?.match(
/(?<![\w.-])v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?![\w.+-])/
)?.[1]
return version && isValidAppVersion(version) ? version : null
}
export function isPrereleaseAppVersion(value: string): boolean {
const parsed = parseVersion(value)
return parsed !== null && parsed.prerelease.length > 0
}
export function isPerfPrereleaseAppVersion(value: string): boolean {
const parsed = parseVersion(value)
return parsed?.prerelease.some((identifier) => identifier.toLowerCase() === 'perf') ?? false
}
function compareIdentifiers(left: string, right: string): number {
const leftNumeric = /^\d+$/.test(left)
const rightNumeric = /^\d+$/.test(right)
if (leftNumeric && rightNumeric) {
return Number(left) - Number(right)
}
if (leftNumeric) {
return -1
}
if (rightNumeric) {
return 1
}
return left.localeCompare(right, 'en')
}
/** Returns negative if left < right, 0 if equal, positive if left > right. */
export function compareAppVersions(left: string, right: string): number {
const leftVersion = parseVersion(left)
const rightVersion = parseVersion(right)
if (!leftVersion || !rightVersion) {
return 0
}
for (let index = 0; index < leftVersion.core.length; index += 1) {
const leftPart = leftVersion.core[index]
const rightPart = rightVersion.core[index]
if (leftPart !== rightPart) {
return leftPart - rightPart
}
}
const leftPrerelease = leftVersion.prerelease
const rightPrerelease = rightVersion.prerelease
if (leftPrerelease.length === 0 && rightPrerelease.length === 0) {
return 0
}
if (leftPrerelease.length === 0) {
return 1
}
if (rightPrerelease.length === 0) {
return -1
}
for (let index = 0; index < Math.max(leftPrerelease.length, rightPrerelease.length); index += 1) {
const leftPart = leftPrerelease[index]
const rightPart = rightPrerelease[index]
if (leftPart === undefined) {
return -1
}
if (rightPart === undefined) {
return 1
}
const comparison = compareIdentifiers(leftPart, rightPart)
if (comparison !== 0) {
return comparison
}
}
return 0
}
/** True when `current` is at or past `target`. Unparseable or absent versions never qualify. */
export function hasReachedAppVersion(current: string, target: string | null): boolean {
return (
target !== null &&
isValidAppVersion(current) &&
isValidAppVersion(target) &&
compareAppVersions(current, target) >= 0
)
}