2025-09-10 01:28:58 -07:00
#!/usr/bin/env node
2025-07-05 13:58:59 -07:00
/ * *
* @ license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-08-25 22:11:27 +02:00
import { execSync } from 'node:child_process' ;
2025-09-10 01:28:58 -07:00
import { fileURLToPath } from 'node:url' ;
2025-09-26 16:35:26 -07:00
import { readFileSync } from 'node:fs' ;
2025-09-24 21:02:00 -07:00
import semver from 'semver' ;
2025-09-10 01:28:58 -07:00
2025-09-26 16:35:26 -07:00
function readJson ( filePath ) {
return JSON . parse ( readFileSync ( filePath , 'utf-8' ) ) ;
}
2025-09-10 01:28:58 -07:00
function getArgs ( ) {
const args = { } ;
process . argv . slice ( 2 ) . forEach ( ( arg ) => {
if ( arg . startsWith ( '--' ) ) {
const [ key , value ] = arg . substring ( 2 ) . split ( '=' ) ;
args [ key ] = value === undefined ? true : value ;
}
} ) ;
return args ;
2025-07-05 13:58:59 -07:00
}
2025-09-10 01:28:58 -07:00
function getLatestTag ( pattern ) {
2025-09-24 21:02:00 -07:00
const command = ` git tag -l ' ${ pattern } ' ` ;
2025-09-10 01:28:58 -07:00
try {
2025-09-24 21:02:00 -07:00
const tags = execSync ( command )
. toString ( )
. trim ( )
. split ( '\n' )
. filter ( Boolean ) ;
if ( tags . length === 0 ) return '' ;
// Convert tags to versions (remove 'v' prefix) and sort by semver
const versions = tags
. map ( ( tag ) => tag . replace ( /^v/ , '' ) )
. filter ( ( version ) => semver . valid ( version ) )
. sort ( ( a , b ) => semver . rcompare ( a , b ) ) ; // rcompare for descending order
if ( versions . length === 0 ) return '' ;
// Return the latest version with 'v' prefix restored
return ` v ${ versions [ 0 ] } ` ;
} catch ( error ) {
console . error (
` Failed to get latest git tag for pattern " ${ pattern } ": ${ error . message } ` ,
) ;
2025-09-10 01:28:58 -07:00
return '' ;
2025-08-20 08:50:00 -07:00
}
}
2025-09-16 23:47:05 -07:00
function getVersionFromNPM ( distTag ) {
const command = ` npm view @google/gemini-cli version --tag= ${ distTag } ` ;
try {
return execSync ( command ) . toString ( ) . trim ( ) ;
2025-09-24 21:02:00 -07:00
} catch ( error ) {
console . error (
` Failed to get NPM version for dist-tag " ${ distTag } ": ${ error . message } ` ,
) ;
2025-09-16 23:47:05 -07:00
return '' ;
}
}
2025-07-05 13:58:59 -07:00
2025-09-24 21:02:00 -07:00
function getAllVersionsFromNPM ( ) {
const command = ` npm view @google/gemini-cli versions --json ` ;
try {
const versionsJson = execSync ( command ) . toString ( ) . trim ( ) ;
return JSON . parse ( versionsJson ) ;
} catch ( error ) {
console . error ( ` Failed to get all NPM versions: ${ error . message } ` ) ;
return [ ] ;
}
}
2025-09-26 16:35:26 -07:00
function isVersionDeprecated ( version ) {
const command = ` npm view @google/gemini-cli@ ${ version } deprecated ` ;
try {
const output = execSync ( command ) . toString ( ) . trim ( ) ;
return output . length > 0 ;
} catch ( error ) {
// This command shouldn't fail for existing versions, but as a safeguard:
2025-09-30 19:16:53 -04:00
console . error (
2025-09-26 16:35:26 -07:00
` Failed to check deprecation status for ${ version } : ${ error . message } ` ,
) ;
return false ; // Assume not deprecated on error to avoid breaking the release.
}
}
2025-09-24 21:02:00 -07:00
function detectRollbackAndGetBaseline ( npmDistTag ) {
// Get the current dist-tag version
const distTagVersion = getVersionFromNPM ( npmDistTag ) ;
if ( ! distTagVersion ) return { baseline : '' , isRollback : false } ;
// Get all published versions
const allVersions = getAllVersionsFromNPM ( ) ;
if ( allVersions . length === 0 )
return { baseline : distTagVersion , isRollback : false } ;
// Filter versions by type to match the dist-tag
let matchingVersions ;
if ( npmDistTag === 'latest' ) {
// Stable versions: no prerelease identifiers
matchingVersions = allVersions . filter (
( v ) => semver . valid ( v ) && ! semver . prerelease ( v ) ,
) ;
} else if ( npmDistTag === 'preview' ) {
// Preview versions: contain -preview
matchingVersions = allVersions . filter (
( v ) => semver . valid ( v ) && v . includes ( '-preview' ) ,
) ;
} else if ( npmDistTag === 'nightly' ) {
// Nightly versions: contain -nightly
matchingVersions = allVersions . filter (
( v ) => semver . valid ( v ) && v . includes ( '-nightly' ) ,
) ;
} else {
// For other dist-tags, just use the dist-tag version
return { baseline : distTagVersion , isRollback : false } ;
}
if ( matchingVersions . length === 0 )
return { baseline : distTagVersion , isRollback : false } ;
2025-09-26 16:35:26 -07:00
// Sort by semver to get a list from highest to lowest
2025-09-24 21:02:00 -07:00
matchingVersions . sort ( ( a , b ) => semver . rcompare ( a , b ) ) ;
2025-09-26 16:35:26 -07:00
// Find the highest non-deprecated version
let highestExistingVersion = '' ;
for ( const version of matchingVersions ) {
if ( ! isVersionDeprecated ( version ) ) {
highestExistingVersion = version ;
break ; // Found the one we want
} else {
2025-09-30 19:16:53 -04:00
console . error ( ` Ignoring deprecated version: ${ version } ` ) ;
2025-09-26 16:35:26 -07:00
}
}
// If all matching versions were deprecated, fall back to the dist-tag version
if ( ! highestExistingVersion ) {
highestExistingVersion = distTagVersion ;
}
2025-09-24 21:02:00 -07:00
// Check if we're in a rollback scenario
const isRollback = semver . gt ( highestExistingVersion , distTagVersion ) ;
return {
baseline : isRollback ? highestExistingVersion : distTagVersion ,
isRollback ,
distTagVersion ,
highestExistingVersion ,
} ;
}
2025-09-26 16:35:26 -07:00
function doesVersionExist ( version ) {
// Check NPM
2025-09-16 23:47:05 -07:00
try {
2025-09-26 16:35:26 -07:00
const command = ` npm view @google/gemini-cli@ ${ version } version 2>/dev/null ` ;
2025-09-16 23:47:05 -07:00
const output = execSync ( command ) . toString ( ) . trim ( ) ;
2025-09-26 16:35:26 -07:00
if ( output === version ) {
2025-09-30 19:16:53 -04:00
console . error ( ` Version ${ version } already exists on NPM. ` ) ;
2025-09-26 16:35:26 -07:00
return true ;
2025-09-12 10:22:10 -07:00
}
2025-09-26 16:35:26 -07:00
} catch ( _error ) {
// This is expected if the version doesn't exist.
2025-09-24 21:02:00 -07:00
}
// Check Git tags
try {
2025-09-26 16:35:26 -07:00
const command = ` git tag -l 'v ${ version } ' ` ;
2025-09-24 21:02:00 -07:00
const tagOutput = execSync ( command ) . toString ( ) . trim ( ) ;
2025-09-26 16:35:26 -07:00
if ( tagOutput === ` v ${ version } ` ) {
2025-09-30 19:16:53 -04:00
console . error ( ` Git tag v ${ version } already exists. ` ) ;
2025-09-26 16:35:26 -07:00
return true ;
2025-09-24 21:02:00 -07:00
}
} catch ( error ) {
2025-09-30 19:16:53 -04:00
console . error ( ` Failed to check git tags for conflicts: ${ error . message } ` ) ;
2025-09-24 21:02:00 -07:00
}
// Check GitHub releases
try {
2025-09-26 16:35:26 -07:00
const command = ` gh release view "v ${ version } " --json tagName --jq .tagName 2>/dev/null ` ;
2025-09-24 21:02:00 -07:00
const output = execSync ( command ) . toString ( ) . trim ( ) ;
2025-09-26 16:35:26 -07:00
if ( output === ` v ${ version } ` ) {
2025-09-30 19:16:53 -04:00
console . error ( ` GitHub release v ${ version } already exists. ` ) ;
2025-09-26 16:35:26 -07:00
return true ;
2025-09-24 21:02:00 -07:00
}
} catch ( error ) {
const isExpectedNotFound =
error . message . includes ( 'release not found' ) ||
error . message . includes ( 'Not Found' ) ||
error . message . includes ( 'not found' ) ||
2025-09-26 16:35:26 -07:00
error . status === 1 ;
2025-09-24 21:02:00 -07:00
if ( ! isExpectedNotFound ) {
2025-09-30 19:16:53 -04:00
console . error (
2025-09-24 21:02:00 -07:00
` Failed to check GitHub releases for conflicts: ${ error . message } ` ,
) ;
}
}
2025-09-26 16:35:26 -07:00
return false ;
2025-09-24 21:02:00 -07:00
}
2025-09-26 16:35:26 -07:00
function getAndVerifyTags ( npmDistTag , _gitTagPattern ) {
2025-09-24 21:02:00 -07:00
// Detect rollback scenarios and get the correct baseline
const rollbackInfo = detectRollbackAndGetBaseline ( npmDistTag ) ;
const baselineVersion = rollbackInfo . baseline ;
if ( ! baselineVersion ) {
throw new Error ( ` Unable to determine baseline version for ${ npmDistTag } ` ) ;
}
2025-09-26 16:35:26 -07:00
if ( rollbackInfo . isRollback ) {
2025-09-24 21:02:00 -07:00
// Rollback scenario: warn about the rollback but don't fail
2025-09-30 19:16:53 -04:00
console . error (
2025-09-24 21:02:00 -07:00
` Rollback detected! NPM ${ npmDistTag } tag is ${ rollbackInfo . distTagVersion } , but using ${ baselineVersion } as baseline for next version calculation (highest existing version). ` ,
2025-09-16 23:47:05 -07:00
) ;
2025-07-05 13:58:59 -07:00
}
2025-09-24 21:02:00 -07:00
2025-09-26 16:35:26 -07:00
// Not verifying against git tags or GitHub releases as per user request.
2025-09-24 21:02:00 -07:00
return {
latestVersion : baselineVersion ,
latestTag : ` v ${ baselineVersion } ` ,
} ;
2025-09-16 23:47:05 -07:00
}
2025-09-26 16:35:26 -07:00
function promoteNightlyVersion ( ) {
const { latestVersion , latestTag } = getAndVerifyTags (
2025-09-16 23:47:05 -07:00
'nightly' ,
'v*-nightly*' ,
) ;
const baseVersion = latestVersion . split ( '-' ) [ 0 ] ;
const versionParts = baseVersion . split ( '.' ) ;
const major = versionParts [ 0 ] ;
const minor = versionParts [ 1 ] ? parseInt ( versionParts [ 1 ] ) : 0 ;
const nextMinor = minor + 1 ;
const date = new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) . replace ( /-/g , '' ) ;
const gitShortHash = execSync ( 'git rev-parse --short HEAD' ) . toString ( ) . trim ( ) ;
return {
releaseVersion : ` ${ major } . ${ nextMinor } .0-nightly. ${ date } . ${ gitShortHash } ` ,
npmTag : 'nightly' ,
previousReleaseTag : latestTag ,
2025-09-26 16:35:26 -07:00
} ;
}
function getNightlyVersion ( ) {
const packageJson = readJson ( 'package.json' ) ;
const baseVersion = packageJson . version . split ( '-' ) [ 0 ] ;
const date = new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) . replace ( /-/g , '' ) ;
const gitShortHash = execSync ( 'git rev-parse --short HEAD' ) . toString ( ) . trim ( ) ;
const releaseVersion = ` ${ baseVersion } -nightly. ${ date } . ${ gitShortHash } ` ;
const previousReleaseTag = getLatestTag ( 'v*-nightly*' ) ;
return {
releaseVersion ,
npmTag : 'nightly' ,
previousReleaseTag ,
2025-09-16 23:47:05 -07:00
} ;
}
2025-09-17 11:02:54 -07:00
function validateVersion ( version , format , name ) {
const versionRegex = {
'X.Y.Z' : /^\d+\.\d+\.\d+$/ ,
'X.Y.Z-preview.N' : /^\d+\.\d+\.\d+-preview\.\d+$/ ,
} ;
if ( ! versionRegex [ format ] || ! versionRegex [ format ] . test ( version ) ) {
throw new Error (
` Invalid ${ name } : ${ version } . Must be in ${ format } format. ` ,
) ;
}
}
function getStableVersion ( args ) {
const { latestVersion : latestPreviewVersion } = getAndVerifyTags (
2025-09-16 23:47:05 -07:00
'preview' ,
'v*-preview*' ,
) ;
2025-09-17 11:02:54 -07:00
let releaseVersion ;
if ( args . stable _version _override ) {
const overrideVersion = args . stable _version _override . replace ( /^v/ , '' ) ;
validateVersion ( overrideVersion , 'X.Y.Z' , 'stable_version_override' ) ;
releaseVersion = overrideVersion ;
} else {
releaseVersion = latestPreviewVersion . replace ( /-preview.*/ , '' ) ;
}
2025-09-26 16:35:26 -07:00
const { latestTag : previousStableTag } = getAndVerifyTags (
2025-09-17 11:02:54 -07:00
'latest' ,
'v[0-9].[0-9].[0-9]' ,
) ;
2025-09-16 23:47:05 -07:00
return {
2025-09-17 11:02:54 -07:00
releaseVersion ,
2025-09-16 23:47:05 -07:00
npmTag : 'latest' ,
2025-09-17 11:02:54 -07:00
previousReleaseTag : previousStableTag ,
2025-09-16 23:47:05 -07:00
} ;
}
2025-07-05 13:58:59 -07:00
2025-09-17 11:02:54 -07:00
function getPreviewVersion ( args ) {
const { latestVersion : latestNightlyVersion } = getAndVerifyTags (
2025-09-16 23:47:05 -07:00
'nightly' ,
'v*-nightly*' ,
) ;
2025-09-17 11:02:54 -07:00
let releaseVersion ;
if ( args . preview _version _override ) {
const overrideVersion = args . preview _version _override . replace ( /^v/ , '' ) ;
validateVersion (
overrideVersion ,
'X.Y.Z-preview.N' ,
'preview_version_override' ,
) ;
releaseVersion = overrideVersion ;
} else {
releaseVersion =
latestNightlyVersion . replace ( /-nightly.*/ , '' ) + '-preview.0' ;
}
2025-09-26 16:35:26 -07:00
const { latestTag : previousPreviewTag } = getAndVerifyTags (
2025-09-17 11:02:54 -07:00
'preview' ,
'v*-preview*' ,
) ;
2025-09-16 23:47:05 -07:00
return {
2025-09-17 11:02:54 -07:00
releaseVersion ,
2025-09-16 23:47:05 -07:00
npmTag : 'preview' ,
2025-09-17 11:02:54 -07:00
previousReleaseTag : previousPreviewTag ,
2025-09-16 23:47:05 -07:00
} ;
}
2025-08-20 08:50:00 -07:00
2025-09-16 23:47:05 -07:00
function getPatchVersion ( patchFrom ) {
if ( ! patchFrom || ( patchFrom !== 'stable' && patchFrom !== 'preview' ) ) {
throw new Error (
'Patch type must be specified with --patch-from=stable or --patch-from=preview' ,
) ;
}
const distTag = patchFrom === 'stable' ? 'latest' : 'preview' ;
const pattern = distTag === 'latest' ? 'v[0-9].[0-9].[0-9]' : 'v*-preview*' ;
2025-09-26 16:35:26 -07:00
const { latestVersion , latestTag } = getAndVerifyTags ( distTag , pattern ) ;
2025-09-18 17:33:08 -07:00
if ( patchFrom === 'stable' ) {
// For stable versions, increment the patch number: 0.5.4 -> 0.5.5
const versionParts = latestVersion . split ( '.' ) ;
const major = versionParts [ 0 ] ;
const minor = versionParts [ 1 ] ;
const patch = versionParts [ 2 ] ? parseInt ( versionParts [ 2 ] ) : 0 ;
const releaseVersion = ` ${ major } . ${ minor } . ${ patch + 1 } ` ;
return {
releaseVersion ,
npmTag : distTag ,
previousReleaseTag : latestTag ,
} ;
} else {
// For preview versions, increment the preview number: 0.6.0-preview.2 -> 0.6.0-preview.3
const [ version , prereleasePart ] = latestVersion . split ( '-' ) ;
if ( ! prereleasePart || ! prereleasePart . startsWith ( 'preview.' ) ) {
throw new Error (
` Invalid preview version format: ${ latestVersion } . Expected format like "0.6.0-preview.2" ` ,
) ;
}
const previewNumber = parseInt ( prereleasePart . split ( '.' ) [ 1 ] ) ;
if ( isNaN ( previewNumber ) ) {
throw new Error ( ` Could not parse preview number from: ${ prereleasePart } ` ) ;
}
const releaseVersion = ` ${ version } -preview. ${ previewNumber + 1 } ` ;
return {
releaseVersion ,
npmTag : distTag ,
previousReleaseTag : latestTag ,
} ;
}
2025-09-16 23:47:05 -07:00
}
export function getVersion ( options = { } ) {
const args = { ... getArgs ( ) , ... options } ;
const type = args . type || 'nightly' ;
let versionData ;
switch ( type ) {
case 'nightly' :
versionData = getNightlyVersion ( ) ;
2025-09-26 16:35:26 -07:00
// Nightly versions include a git hash, so conflicts are highly unlikely
// and indicate a problem. We'll still validate but not auto-increment.
if ( doesVersionExist ( versionData . releaseVersion ) ) {
throw new Error (
` Version conflict! Nightly version ${ versionData . releaseVersion } already exists. ` ,
) ;
}
break ;
case 'promote-nightly' :
versionData = promoteNightlyVersion ( ) ;
2025-09-16 23:47:05 -07:00
break ;
case 'stable' :
2025-09-17 11:02:54 -07:00
versionData = getStableVersion ( args ) ;
2025-09-16 23:47:05 -07:00
break ;
case 'preview' :
2025-09-17 11:02:54 -07:00
versionData = getPreviewVersion ( args ) ;
2025-09-16 23:47:05 -07:00
break ;
case 'patch' :
versionData = getPatchVersion ( args [ 'patch-from' ] ) ;
break ;
default :
throw new Error ( ` Unknown release type: ${ type } ` ) ;
}
2025-09-26 16:35:26 -07:00
// For patchable versions, check for existence and increment if needed.
if ( type === 'stable' || type === 'preview' || type === 'patch' ) {
let releaseVersion = versionData . releaseVersion ;
while ( doesVersionExist ( releaseVersion ) ) {
2025-09-30 19:16:53 -04:00
console . error ( ` Version ${ releaseVersion } exists, incrementing. ` ) ;
2025-09-26 16:35:26 -07:00
if ( releaseVersion . includes ( '-preview.' ) ) {
// Increment preview number: 0.6.0-preview.2 -> 0.6.0-preview.3
const [ version , prereleasePart ] = releaseVersion . split ( '-' ) ;
const previewNumber = parseInt ( prereleasePart . split ( '.' ) [ 1 ] ) ;
releaseVersion = ` ${ version } -preview. ${ previewNumber + 1 } ` ;
} else {
// Increment patch number: 0.5.4 -> 0.5.5
const versionParts = releaseVersion . split ( '.' ) ;
const major = versionParts [ 0 ] ;
const minor = versionParts [ 1 ] ;
const patch = parseInt ( versionParts [ 2 ] ) ;
releaseVersion = ` ${ major } . ${ minor } . ${ patch + 1 } ` ;
}
}
versionData . releaseVersion = releaseVersion ;
}
2025-09-24 21:02:00 -07:00
2025-09-26 16:35:26 -07:00
// All checks are done, construct the final result.
2025-09-24 21:02:00 -07:00
const result = {
2025-09-16 23:47:05 -07:00
releaseTag : ` v ${ versionData . releaseVersion } ` ,
... versionData ,
2025-09-10 01:28:58 -07:00
} ;
2025-09-24 21:02:00 -07:00
return result ;
2025-07-05 13:58:59 -07:00
}
2025-09-10 01:28:58 -07:00
if ( process . argv [ 1 ] === fileURLToPath ( import . meta . url ) ) {
2025-09-16 23:47:05 -07:00
console . log ( JSON . stringify ( getVersion ( getArgs ( ) ) , null , 2 ) ) ;
2025-07-05 13:58:59 -07:00
}