@@ -26,10 +26,100 @@ interface GhRun {
2626 url : string ;
2727}
2828
29+ interface CommandResult {
30+ exitCode : number ;
31+ stdout : string ;
32+ stderr : string ;
33+ }
34+
2935const CI_WORKFLOW = "ci.yml" ;
3036const CI_WAIT_TIMEOUT_MS = 20 * 60 * 1000 ;
3137const CI_POLL_MS = 10 * 1000 ;
3238
39+ async function runQuiet ( command : string [ ] ) : Promise < CommandResult > {
40+ const proc = Bun . spawn ( command , { stdout : "pipe" , stderr : "pipe" } ) ;
41+ const [ stdout , stderr , exitCode ] = await Promise . all ( [
42+ new Response ( proc . stdout ) . text ( ) ,
43+ new Response ( proc . stderr ) . text ( ) ,
44+ proc . exited ,
45+ ] ) ;
46+ return { exitCode, stdout : stdout . trim ( ) , stderr : stderr . trim ( ) } ;
47+ }
48+
49+ async function readPackageName ( ) : Promise < string > {
50+ try {
51+ const pkg = JSON . parse ( await Bun . file ( "package.json" ) . text ( ) ) as { name ?: unknown } ;
52+ if ( typeof pkg . name !== "string" || ! pkg . name ) {
53+ console . error ( "✗ package.json is missing a valid name" ) ;
54+ process . exit ( 1 ) ;
55+ }
56+ return pkg . name ;
57+ } catch ( error ) {
58+ console . error ( `✗ failed to read package.json: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
59+ process . exit ( 1 ) ;
60+ }
61+ }
62+
63+ async function npmVersionExists ( packageName : string , version : string ) : Promise < boolean > {
64+ const result = await runQuiet ( [ "npm" , "view" , `${ packageName } @${ version } ` , "version" ] ) ;
65+ if ( result . exitCode === 0 ) return true ;
66+
67+ const output = `${ result . stdout } \n${ result . stderr } ` ;
68+ if ( output . includes ( "E404" ) || output . includes ( "No match found" ) ) return false ;
69+
70+ console . error ( `✗ failed to check npm version ${ packageName } @${ version } ` ) ;
71+ if ( result . stderr ) console . error ( result . stderr ) ;
72+ process . exit ( 1 ) ;
73+ }
74+
75+ async function remoteTagSha ( tagName : string ) : Promise < string | null > {
76+ const result = await runQuiet ( [ "git" , "ls-remote" , "origin" , `refs/tags/${ tagName } ` , `refs/tags/${ tagName } ^{}` ] ) ;
77+ if ( result . exitCode !== 0 ) {
78+ console . error ( `✗ failed to check remote tag ${ tagName } ` ) ;
79+ if ( result . stderr ) console . error ( result . stderr ) ;
80+ process . exit ( 1 ) ;
81+ }
82+
83+ const lines = result . stdout . split ( "\n" ) . filter ( Boolean ) ;
84+ const peeled = lines . find ( line => line . endsWith ( `refs/tags/${ tagName } ^{}` ) ) ;
85+ const exact = lines . find ( line => line . endsWith ( `refs/tags/${ tagName } ` ) ) ;
86+ const selected = peeled ?? exact ;
87+ return selected ? selected . split ( / \s + / ) [ 0 ] ?? null : null ;
88+ }
89+
90+ async function githubReleaseExists ( tagName : string ) : Promise < boolean > {
91+ const result = await runQuiet ( [ "gh" , "release" , "view" , tagName , "--json" , "tagName" ] ) ;
92+ if ( result . exitCode === 0 ) return true ;
93+
94+ const output = `${ result . stdout } \n${ result . stderr } ` . toLowerCase ( ) ;
95+ if ( output . includes ( "release not found" ) || output . includes ( "not found" ) ) return false ;
96+
97+ console . error ( `✗ failed to check GitHub Release ${ tagName } ` ) ;
98+ if ( result . stderr ) console . error ( result . stderr ) ;
99+ process . exit ( 1 ) ;
100+ }
101+
102+ async function assertUnusedReleaseVersion ( packageName : string , version : string ) : Promise < void > {
103+ const releaseTag = `v${ version } ` ;
104+ const [ npmUsed , tagSha , releaseUsed ] = await Promise . all ( [
105+ npmVersionExists ( packageName , version ) ,
106+ remoteTagSha ( releaseTag ) ,
107+ githubReleaseExists ( releaseTag ) ,
108+ ] ) ;
109+
110+ const failures : string [ ] = [ ] ;
111+ if ( npmUsed ) failures . push ( `- npm already has ${ packageName } @${ version } ` ) ;
112+ if ( tagSha ) failures . push ( `- remote Git tag ${ releaseTag } already exists at ${ tagSha } ` ) ;
113+ if ( releaseUsed ) failures . push ( `- GitHub Release ${ releaseTag } already exists` ) ;
114+
115+ if ( failures . length > 0 ) {
116+ console . error ( `✗ release version ${ version } is already partially or fully used:` ) ;
117+ console . error ( failures . join ( "\n" ) ) ;
118+ console . error ( "Choose the next unused patch version, or make an explicit human decision to repair public metadata." ) ;
119+ process . exit ( 1 ) ;
120+ }
121+ }
122+
33123async function watchLatest ( ) : Promise < void > {
34124 const id = ( await $ `gh run list --workflow release.yml --limit 1 --json databaseId -q '.[0].databaseId'` . text ( ) ) . trim ( ) ;
35125 if ( ! id ) { console . error ( "No Release runs found yet." ) ; process . exit ( 1 ) ; }
@@ -99,6 +189,9 @@ const dryRun = !args.includes("--publish");
99189const branch = ( await $ `git rev-parse --abbrev-ref HEAD` . text ( ) ) . trim ( ) ;
100190if ( branch !== "main" ) { console . error ( `✗ must be on main (currently ${ branch } ).` ) ; process . exit ( 1 ) ; }
101191if ( ( await $ `git status --porcelain` . text ( ) ) . trim ( ) ) { console . error ( "✗ working tree not clean — commit or stash first." ) ; process . exit ( 1 ) ; }
192+ const packageName = await readPackageName ( ) ;
193+ console . log ( `→ release metadata preflight (${ packageName } @${ version } )` ) ;
194+ await assertUnusedReleaseVersion ( packageName , version ) ;
102195console . log ( "→ typecheck" ) ;
103196await $ `bun x tsc --noEmit` ;
104197
0 commit comments