11import { $ } from "bun" ;
22import pRetry from "p-retry" ;
3+ import { SemVer } from "semver" ;
4+
5+ /**
6+ * Represents the output of the `anvil --version` command after parsing it into a structured format.
7+ * Type defined based on anvil version 1.4.3-v1.4.3 output.
8+ *
9+ * @example
10+ * // anvil Version: 1.4.3-v1.4.3
11+ * // Commit SHA: fa9f934bdac4bcf57e694e852a61997dda90668a
12+ * // Build Timestamp: 2025-10-22T04:37:38.758664000Z (1761107858)
13+ * // Build Profile: maxperf
14+ *
15+ */
16+ type AnvilVersionStdout = {
17+ anvil_version : string ;
18+ commit_sha : string ;
19+ build_timestamp : string ;
20+ build_profile : string ;
21+ } ;
22+
23+ type AnvilVersionStdoutKeys = keyof AnvilVersionStdout ;
24+
25+ const normalizeAnvilVersionOutput = ( output : string ) => {
26+ return output . split ( "\n" ) . reduce (
27+ ( curr , next ) => {
28+ const trimmedLine = next . trim ( ) ;
29+ const firstColonIndex = trimmedLine . indexOf ( ":" ) ;
30+
31+ if ( firstColonIndex === - 1 ) return curr ;
32+
33+ const prop = trimmedLine . slice ( 0 , firstColonIndex ) ;
34+ const value = trimmedLine . slice ( firstColonIndex + 1 ) ;
35+ const key = prop
36+ . trim ( )
37+ . toLowerCase ( )
38+ . replace ( / \s + / g, "_" ) as AnvilVersionStdoutKeys ;
39+
40+ if ( key && value ) {
41+ curr [ key ] = value . trim ( ) ;
42+ }
43+
44+ return curr ;
45+ } ,
46+ { } as Partial < AnvilVersionStdout > ,
47+ ) ;
48+ } ;
49+
50+ /**
51+ * Get the clean version of an anvil version string.
52+ * Anvil uses semver with a quirk by suffixing the version with tag-names e.g. 1.4.3-v1.4.3,
53+ * so we need to parse it with semver and get the clean version.
54+ * @param anvilVersion
55+ * @returns
56+ */
57+ const getCleanVersion = ( anvilVersion : string ) => {
58+ try {
59+ const semver = new SemVer ( anvilVersion , { loose : true } ) ;
60+ return `${ semver . major } .${ semver . minor } .${ semver . patch } ` ;
61+ } catch {
62+ // If the version is not a valid semver, return null
63+ return null ;
64+ }
65+ } ;
366
467/**
568 * Get the installed anvil version
@@ -8,19 +71,21 @@ import pRetry from "p-retry";
871export const version = async ( ) => {
972 const output = await $ `anvil --version` . text ( ) ;
1073
11- // anvil Version: 1.4.3-v1.4.3
12- // Commit SHA: fa9f934bdac4bcf57e694e852a61997dda90668a
13- // Build Timestamp: 2025-10-22T04:37:38.758664000Z (1761107858)
14- // Build Profile: maxperf
74+ const data = normalizeAnvilVersionOutput ( output ) ;
1575
16- // parse the output to get the version
17- const versionMatch = output . match (
18- / V e r s i o n : ( \d + \. \d + \. \d + ) - v ( \d + \. \d + \. \d + ) / ,
19- ) ;
20- if ( ! versionMatch ) {
76+ if ( ! data . anvil_version ) {
2177 throw new Error ( "Failed to parse anvil version. Is anvil installed?" ) ;
2278 }
23- return versionMatch [ 1 ] ;
79+
80+ const cleanVersion = getCleanVersion ( data . anvil_version ) ;
81+
82+ if ( ! cleanVersion ) {
83+ throw new Error (
84+ `Anvil version "${ data . anvil_version } " is not a valid semver.` ,
85+ ) ;
86+ }
87+
88+ return cleanVersion ;
2489} ;
2590
2691type StartOptions = {
0 commit comments