version.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. #!/usr/bin/env bun
  2. /**
  3. * Version detection utilities for upstream merge automation
  4. */
  5. import { $ } from "bun"
  6. import { getUpstreamTags, getCommitMessage, getTagsForCommit } from "./git"
  7. export interface VersionInfo {
  8. version: string
  9. tag: string
  10. commit: string
  11. }
  12. /**
  13. * Parse version from a tag string (e.g., "v1.1.49" -> "1.1.49")
  14. * Only matches stable versions (not dev/preview tags like v0.0.0-202507310417)
  15. */
  16. export function parseVersion(tag: string, includePrerelease = false): string | null {
  17. // Match stable versions like v1.1.49 or 1.1.49
  18. const stableMatch = tag.match(/^v?(\d+\.\d+\.\d+)$/)
  19. if (stableMatch) return stableMatch[1] ?? null
  20. // Optionally match prerelease versions
  21. if (includePrerelease) {
  22. const prereleaseMatch = tag.match(/^v?(\d+\.\d+\.\d+-.+)$/)
  23. if (prereleaseMatch) return prereleaseMatch[1] ?? null
  24. }
  25. return null
  26. }
  27. /**
  28. * Compare two semver versions
  29. * Returns: -1 if a < b, 0 if a == b, 1 if a > b
  30. */
  31. export function compareVersions(a: string, b: string): number {
  32. const partsA = a.split(".").map((x) => parseInt(x, 10) || 0)
  33. const partsB = b.split(".").map((x) => parseInt(x, 10) || 0)
  34. for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
  35. const numA = partsA[i] || 0
  36. const numB = partsB[i] || 0
  37. if (numA < numB) return -1
  38. if (numA > numB) return 1
  39. }
  40. return 0
  41. }
  42. /**
  43. * Get the latest upstream version from tags
  44. */
  45. export async function getLatestUpstreamVersion(): Promise<VersionInfo | null> {
  46. const versions = await getAvailableUpstreamVersions()
  47. if (versions.length === 0) return null
  48. return versions[0] ?? null
  49. }
  50. /**
  51. * Get version info for a specific commit
  52. */
  53. export async function getVersionForCommit(commit: string): Promise<VersionInfo | null> {
  54. const tags = await getTagsForCommit(commit)
  55. for (const tag of tags) {
  56. const version = parseVersion(tag)
  57. if (version) {
  58. return { version, tag, commit }
  59. }
  60. }
  61. // Try to extract from commit message
  62. const message = await getCommitMessage(commit)
  63. const match = message.match(/v?(\d+\.\d+\.\d+)/)
  64. if (match && match[1]) {
  65. return {
  66. version: match[1],
  67. tag: `v${match[1]}`,
  68. commit,
  69. }
  70. }
  71. return null
  72. }
  73. /**
  74. * Get available upstream versions (sorted newest first)
  75. */
  76. export async function getAvailableUpstreamVersions(): Promise<VersionInfo[]> {
  77. // Get tags with their commits directly from ls-remote
  78. const result = await $`git ls-remote --tags upstream`.quiet().nothrow()
  79. if (result.exitCode !== 0) {
  80. throw new Error(`Failed to list upstream tags: ${result.stderr.toString()}`)
  81. }
  82. const output = result.stdout.toString()
  83. const versions: VersionInfo[] = []
  84. for (const line of output.trim().split("\n")) {
  85. // Match lines like: abc123... refs/tags/v1.1.49
  86. // Skip annotated tag references (those ending with ^{})
  87. const match = line.match(/^([a-f0-9]+)\s+refs\/tags\/([^\^]+)$/)
  88. if (!match) continue
  89. const commit = match[1]
  90. const tag = match[2]
  91. if (!commit || !tag) continue
  92. const version = parseVersion(tag)
  93. if (version) {
  94. versions.push({ version, tag, commit })
  95. }
  96. }
  97. // Sort by version descending
  98. versions.sort((a, b) => compareVersions(b.version, a.version))
  99. return versions
  100. }
  101. /**
  102. * Get current Kilo version from package.json
  103. */
  104. export async function getCurrentKiloVersion(): Promise<string> {
  105. // Resolve path relative to repo root (script is in script/upstream/)
  106. const path = new URL("../../../packages/opencode/package.json", import.meta.url).pathname
  107. const pkg = await Bun.file(path).json()
  108. return pkg.version
  109. }