2025-07-31 05:00:29 +00:00
# ! / u s r / b i n / e n v b u n
2026-02-23 23:15:25 +00:00
import { fileURLToPath } from "url"
2025-07-31 05:00:29 +00:00
2026-02-23 23:15:25 +00:00
const dir = fileURLToPath ( new URL ( ".." , import . meta . url ) )
2025-07-31 05:00:29 +00:00
process . chdir ( dir )
import { $ } from "bun"
import path from "path"
import { createClient } from "@hey-api/openapi-ts"
2026-04-28 15:02:35 +00:00
const opencode = path . resolve ( dir , "../../opencode" )
2026-07-02 22:15:18 +00:00
const client = path . resolve ( dir , "../../client" )
2026-04-28 15:02:35 +00:00
2026-07-22 00:13:27 +00:00
if ( ! ( await Bun . file ( path . join ( opencode , "package.json" ) ) . exists ( ) ) ) {
await $ ` rm -rf dist `
await $ ` bun tsc `
process . exit ( 0 )
}
2026-05-09 13:10:42 +00:00
await $ ` bun dev generate > ${ dir } /openapi.json ` . cwd ( opencode )
2026-07-02 22:15:18 +00:00
await $ ` bun -e ${ `
import { OpenApi } from "effect/unstable/httpapi"
import { ClientApi } from "@opencode-ai/protocol/client"
2025-07-31 05:00:29 +00:00
2026-07-02 22:15:18 +00:00
const output = process . argv . at ( - 1 )
if ( ! output ) throw new Error ( "Missing OpenAPI output path" )
await Bun . write ( output , JSON . stringify ( OpenApi . fromApi ( ClientApi ) ) )
` } ${ path . join ( dir , "openapi-v2.json" ) } ` . cwd ( client )
type OpenApiDocument = {
2026-06-28 15:30:38 +00:00
components ? : { schemas? : Record < string , unknown > }
2026-07-02 22:15:18 +00:00
paths? : Record < string , unknown >
2026-06-28 15:30:38 +00:00
[ key : string ] : unknown
}
2026-07-02 22:15:18 +00:00
const document = ( await Bun . file ( "./openapi.json" ) . json ( ) ) as OpenApiDocument
const v2Document = ( await Bun . file ( "./openapi-v2.json" ) . json ( ) ) as OpenApiDocument
2026-07-06 22:14:06 +00:00
normalizeComponentNames ( v2Document )
deduplicateEquivalentComponent ( v2Document , "Shell" , "Shell1" )
2026-07-02 22:15:18 +00:00
renameCollidingComponents ( document , v2Document )
document . paths = { . . . document . paths , . . . v2Document . paths }
document . components = {
. . . document . components ,
schemas : { . . . document . components ? . schemas , . . . v2Document . components ? . schemas } ,
}
inlineTypedAllOfConstraints ( document )
2026-06-28 15:30:38 +00:00
const schemas = document . components ? . schemas
if ( schemas ) {
const reachable = new Set < string > ( )
const visit = ( value : unknown ) = > {
if ( Array . isArray ( value ) ) {
value . forEach ( visit )
return
}
if ( typeof value !== "object" || value === null ) return
for ( const [ key , child ] of Object . entries ( value ) ) {
if ( key === "$ref" && typeof child === "string" && child . startsWith ( "#/components/schemas/" ) ) {
const name = child . slice ( "#/components/schemas/" . length )
if ( reachable . has ( name ) ) continue
reachable . add ( name )
visit ( schemas [ name ] )
} else {
visit ( child )
}
}
}
visit ( { . . . document , components : { . . . document . components , schemas : undefined } } )
for ( const name of Object . keys ( schemas ) ) {
2026-07-03 18:25:59 +00:00
if (
2026-07-09 02:07:45 +00:00
/^(SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionInputPromoted|SessionInputAdmitted|SessionExecutionStarted|SessionExecutionSucceeded|SessionExecutionFailed|SessionExecutionInterrupted|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetryScheduled|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)\d+$/ . test (
2026-07-03 18:25:59 +00:00
name ,
) &&
! reachable . has ( name )
)
delete schemas [ name ]
2026-06-28 15:30:38 +00:00
}
await Bun . write ( "./openapi.json" , JSON . stringify ( document ) )
}
2025-07-31 05:00:29 +00:00
await createClient ( {
input : "./openapi.json" ,
2025-08-09 00:05:50 +00:00
output : {
2025-12-08 00:04:14 +00:00
path : "./src/v2/gen" ,
2025-09-10 03:43:37 +00:00
tsConfigPath : path.join ( dir , "tsconfig.json" ) ,
2025-12-08 00:04:14 +00:00
clean : true ,
2025-08-09 00:05:50 +00:00
} ,
2025-07-31 05:00:29 +00:00
plugins : [
{
name : "@hey-api/typescript" ,
exportFromIndex : false ,
} ,
{
name : "@hey-api/sdk" ,
instance : "OpencodeClient" ,
exportFromIndex : false ,
auth : false ,
2025-12-08 00:04:14 +00:00
paramsStructure : "flat" ,
2025-07-31 05:00:29 +00:00
} ,
{
name : "@hey-api/client-fetch" ,
exportFromIndex : false ,
baseUrl : "http://localhost:4096" ,
} ,
] ,
} )
2025-12-08 00:04:14 +00:00
2026-07-03 05:32:10 +00:00
const generatedTypesPath = "./src/v2/gen/types.gen.ts"
const generatedTypes = await Bun . file ( generatedTypesPath ) . text ( )
2026-07-03 18:25:59 +00:00
if (
2026-07-09 02:07:45 +00:00
/export type (SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionInputPromoted|SessionInputAdmitted|SessionExecutionStarted|SessionExecutionSucceeded|SessionExecutionFailed|SessionExecutionInterrupted|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetryScheduled|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)\d+ =/ . test (
2026-07-03 18:25:59 +00:00
generatedTypes ,
)
) {
2026-06-28 15:30:38 +00:00
throw new Error ( "Session history generated duplicate Session event variants" )
}
2026-07-06 22:14:06 +00:00
const sessionErrorTypesPatched = deduplicateEquivalentGeneratedTypes (
generatedTypes ,
"SessionStructuredError" ,
/^SessionStructuredError\d+$/ ,
)
const obsoleteSessionNext = [ . . . sessionErrorTypesPatched . matchAll ( /export type (SessionNext\w*) =/g ) ] . map (
( match ) = > match [ 1 ] ,
)
if ( obsoleteSessionNext . length > 0 ) {
throw new Error ( ` Obsolete SessionNext generated type noise reintroduced: ${ obsoleteSessionNext . join ( ", " ) } ` )
}
const logTypesPatched = sessionErrorTypesPatched . replace (
2026-07-02 22:15:18 +00:00
/(export type V2SessionLogData = \{[\s\S]*?query\?: \{\s*after\?: )string/ ,
"$1number" ,
2026-06-28 15:30:38 +00:00
)
2026-07-06 22:14:06 +00:00
if ( logTypesPatched === sessionErrorTypesPatched ) {
2026-07-02 22:15:18 +00:00
throw new Error ( "Session log numeric query patch did not apply" )
2026-06-28 15:30:38 +00:00
}
2026-07-03 05:32:10 +00:00
const sessionListTypesPatched = logTypesPatched . replace (
/(export type V2SessionListData = \{[\s\S]*?query\?: \{[\s\S]*?limit\?: )string( \| null)/ ,
"$1number$2" ,
)
if ( sessionListTypesPatched === logTypesPatched ) {
throw new Error ( "Session list numeric query patch did not apply" )
}
const sessionMessagesTypesPatched = sessionListTypesPatched . replace (
2026-07-07 23:56:47 +00:00
/(export type V2MessageListData = \{[\s\S]*?query\?: \{[\s\S]*?limit\?: )string( \| null)/ ,
2026-07-03 05:32:10 +00:00
"$1number$2" ,
)
if ( sessionMessagesTypesPatched === sessionListTypesPatched ) {
throw new Error ( "Session messages numeric query patch did not apply" )
}
const eventSubscribeTypesPatched = sessionMessagesTypesPatched . replace (
2026-07-17 14:31:27 +00:00
/(export type V2EventSubscribeResponses = \{\s*\/\*\*[\s\S]*?\*\/\s*200: )\{\s*id: string \| null;?\s*event: string;?\s*data: (?:V2EventStream(?:V2)?|V2EventJsonString);?\s*\};?/ ,
2026-07-03 05:32:10 +00:00
"$1V2Event" ,
)
if ( eventSubscribeTypesPatched === sessionMessagesTypesPatched ) {
throw new Error ( "Event subscribe response patch did not apply" )
}
2026-07-06 22:14:06 +00:00
if ( /SessionStructuredError\d/ . test ( eventSubscribeTypesPatched ) ) {
throw new Error ( "Session structured error generated a name-mangled duplicate" )
}
if ( /\bSessionNext\w*\b/ . test ( eventSubscribeTypesPatched ) ) {
throw new Error ( "Obsolete SessionNext generated type noise reintroduced" )
}
if ( /export type Shell\d+V2 =/ . test ( eventSubscribeTypesPatched ) ) {
throw new Error ( "Shell generated a name-mangled duplicate" )
}
2026-07-03 05:32:10 +00:00
await Bun . write ( generatedTypesPath , eventSubscribeTypesPatched )
2026-06-28 15:30:38 +00:00
2026-07-02 23:48:49 +00:00
const querySerializerPath = "./src/v2/gen/client/utils.gen.ts"
const querySerializerSource = await Bun . file ( querySerializerPath ) . text ( )
const querySerializerPatched = querySerializerSource . replace (
/if \(value === undefined \|\| value === null\) \{\s*continue;?\s*\}/ ,
"if (value === undefined) {\n continue;\n }\n\n if (value === null) {\n search.push(`${name}=null`);\n continue;\n }" ,
)
if ( querySerializerPatched === querySerializerSource ) {
throw new Error (
` Query serializer null patch did not apply; @hey-api/openapi-ts output may have changed ( ${ querySerializerPath } ) ` ,
)
}
await Bun . write ( querySerializerPath , querySerializerPatched )
2026-07-03 05:32:10 +00:00
const generatedSdkPath = "./src/v2/gen/sdk.gen.ts"
const generatedSdk = await Bun . file ( generatedSdkPath ) . text ( )
2026-07-02 22:15:18 +00:00
const logSdkPatched = generatedSdk . replace (
/(Read the session log[\s\S]*?parameters: \{[\s\S]*?after\?: )string(\s*\|\s*null)?/ ,
"$1number$2" ,
2026-06-28 15:30:38 +00:00
)
2026-07-02 22:15:18 +00:00
if ( logSdkPatched === generatedSdk ) {
throw new Error ( "Session log numeric SDK patch did not apply" )
2026-06-28 15:30:38 +00:00
}
2026-07-03 05:32:10 +00:00
const sessionListSdkPatched = logSdkPatched . replace (
/(List sessions[\s\S]*?parameters\?: \{[\s\S]*?limit\?: )string( \| null)/ ,
"$1number$2" ,
)
if ( sessionListSdkPatched === logSdkPatched ) {
throw new Error ( "Session list numeric SDK patch did not apply" )
}
const sessionMessagesSdkPatched = sessionListSdkPatched . replace (
/(Get session messages[\s\S]*?parameters: \{[\s\S]*?limit\?: )string( \| null)/ ,
"$1number$2" ,
)
if ( sessionMessagesSdkPatched === sessionListSdkPatched ) {
throw new Error ( "Session messages numeric SDK patch did not apply" )
}
await Bun . write ( generatedSdkPath , sessionMessagesSdkPatched )
2026-06-28 15:30:38 +00:00
2026-05-20 18:29:19 +00:00
// Patch a @hey-api/openapi-ts codegen bug: SseFn incorrectly passes the
// endpoint's TError into the second generic of ServerSentEventsResult, which
// is the AsyncGenerator's TReturn slot. Iterator return values have nothing
// to do with HTTP errors, and any consumer that calls `.return()` or returns
// from a mock generator gets type-checked against the wrong shape. Drop the
// arg so TReturn defaults to void.
const sseTypesPath = "./src/v2/gen/client/types.gen.ts"
const sseTypesFile = Bun . file ( sseTypesPath )
const sseTypesSource = await sseTypesFile . text ( )
const sseTypesPatched = sseTypesSource . replace (
"=> Promise<ServerSentEventsResult<TData, TError>>" ,
"=> Promise<ServerSentEventsResult<TData>>" ,
)
if ( sseTypesPatched === sseTypesSource ) {
throw new Error ( ` SseFn patch did not apply; @hey-api/openapi-ts output may have changed ( ${ sseTypesPath } ) ` )
}
await Bun . write ( sseTypesPath , sseTypesPatched )
2025-08-02 22:50:19 +00:00
await $ ` bun prettier --write src/gen `
2025-12-08 00:04:14 +00:00
await $ ` bun prettier --write src/v2 `
2025-09-27 08:10:56 +00:00
await $ ` rm -rf dist `
await $ ` bun tsc `
2026-07-02 22:15:18 +00:00
await $ ` rm openapi.json openapi-v2.json `
function renameCollidingComponents ( target : OpenApiDocument , source : OpenApiDocument ) {
const targetSchemas = target . components ? . schemas
const sourceSchemas = source . components ? . schemas
if ( ! targetSchemas || ! sourceSchemas ) return
const renames = new Map < string , string > ( )
for ( const name of Object . keys ( sourceSchemas ) ) {
if ( ! Object . hasOwn ( targetSchemas , name ) ) continue
2026-07-06 22:14:06 +00:00
if ( JSON . stringify ( normalizeSchema ( sourceSchemas [ name ] ) ) === JSON . stringify ( normalizeSchema ( targetSchemas [ name ] ) ) ) {
delete sourceSchemas [ name ]
continue
}
2026-07-02 22:15:18 +00:00
let renamed = ` ${ name } V2 `
let index = 2
while ( Object . hasOwn ( targetSchemas , renamed ) || Object . hasOwn ( sourceSchemas , renamed ) ) {
renamed = ` ${ name } V2 ${ index } `
index ++
}
renames . set ( name , renamed )
}
if ( renames . size === 0 ) return
source . components = {
. . . source . components ,
schemas : Object.fromEntries (
Object . entries ( sourceSchemas ) . map ( ( [ name , schema ] ) = > [ renames . get ( name ) ? ? name , rewriteRefs ( schema , renames ) ] ) ,
) ,
}
source . paths = rewriteRefs ( source . paths , renames ) as Record < string , unknown > | undefined
}
2026-07-06 22:14:06 +00:00
function normalizeComponentNames ( document : OpenApiDocument ) {
const schemas = document . components ? . schemas
if ( ! schemas ) return
const canonical = new Map ( Object . entries ( schemas ) )
const renames = new Map < string , string > ( )
for ( const name of Object . keys ( schemas ) ) {
const next = componentTypeName ( name )
if ( next === name ) continue
const existing = canonical . get ( next )
if ( existing !== undefined ) {
if ( JSON . stringify ( normalizeSchema ( schemas [ name ] ) ) !== JSON . stringify ( normalizeSchema ( existing ) ) ) continue
renames . set ( name , next )
continue
}
renames . set ( name , next )
canonical . set ( next , schemas [ name ] )
}
if ( renames . size === 0 ) return
const renamed = new Set < string > ( )
document . components = {
. . . document . components ,
schemas : Object.fromEntries (
[
. . . Object . entries ( schemas ) . filter ( ( [ name ] ) = > ! renames . has ( name ) ) ,
. . . Object . entries ( schemas ) . flatMap ( ( [ name , schema ] ) = > {
const next = renames . get ( name )
if ( ! next || Object . hasOwn ( schemas , next ) || renamed . has ( next ) ) return [ ]
renamed . add ( next )
return [ [ next , schema ] as const ]
} ) ,
] . map ( ( [ name , schema ] ) = > [ name , rewriteRefs ( schema , renames ) ] ) ,
) ,
}
document . paths = rewriteRefs ( document . paths , renames ) as Record < string , unknown > | undefined
}
function componentTypeName ( name : string ) {
if ( ! name . includes ( "." ) ) return name
return name
. split ( "." )
. filter ( ( part ) = > ! /^\d+$/ . test ( part ) )
. map ( ( part ) = > part . slice ( 0 , 1 ) . toUpperCase ( ) + part . slice ( 1 ) )
. join ( "" )
}
function deduplicateEquivalentComponent ( document : OpenApiDocument , canonical : string , duplicate : string ) {
const schemas = document . components ? . schemas
if ( ! schemas ? . [ canonical ] || ! schemas [ duplicate ] ) return
if ( JSON . stringify ( normalizeSchema ( schemas [ canonical ] ) ) !== JSON . stringify ( normalizeSchema ( schemas [ duplicate ] ) ) ) {
throw new Error ( ` ${ duplicate } no longer has the same wire shape as ${ canonical } ` )
}
const renames = new Map ( [ [ duplicate , canonical ] ] )
const rewritten = rewriteRefs ( schemas , renames ) as Record < string , unknown >
delete rewritten [ duplicate ]
document . components = { . . . document . components , schemas : rewritten }
document . paths = rewriteRefs ( document . paths , renames ) as Record < string , unknown > | undefined
}
function deduplicateEquivalentGeneratedTypes ( source : string , canonical : string , duplicates : RegExp ) {
const canonicalType = generatedType ( source , canonical )
if ( ! canonicalType ) throw new Error ( ` Generated canonical type missing: ${ canonical } ` )
const names = [ . . . source . matchAll ( /export type (\w+) =/g ) ]
. map ( ( match ) = > match [ 1 ] )
. filter ( ( name ) : name is string = > name !== undefined && duplicates . test ( name ) )
return names . reduce ( ( patched , name ) = > {
const duplicate = generatedType ( patched , name )
const currentCanonical = generatedType ( patched , canonical )
if ( ! duplicate || ! currentCanonical ) throw new Error ( ` Generated type declaration missing while comparing ${ name } ` )
if ( normalizeGeneratedType ( currentCanonical . shape ) !== normalizeGeneratedType ( duplicate . shape ) ) {
throw new Error ( ` ${ name } no longer has the same generated type shape as ${ canonical } ` )
}
return ( patched . slice ( 0 , duplicate . start ) + patched . slice ( duplicate . end ) ) . replaceAll ( name , canonical )
} , source )
}
function generatedType ( source : string , name : string ) {
const start = source . indexOf ( ` export type ${ name } = ` )
if ( start === - 1 ) return undefined
const next = source . indexOf ( "\n\nexport type " , start + 1 )
const shapeEnd = next === - 1 ? source.length : next
return {
start ,
end : next === - 1 ? source.length : next + 2 ,
shape : source.slice ( source . indexOf ( "=" , start ) + 1 , shapeEnd ) ,
}
}
function normalizeGeneratedType ( shape : string ) {
return shape . replaceAll ( /\s/g , "" )
}
function normalizeSchema ( value : unknown , key? : string ) : unknown {
if ( Array . isArray ( value ) ) {
const flattened =
key === "anyOf"
? value . flatMap ( ( item ) = >
typeof item === "object" && item !== null && Object . keys ( item ) . length === 1 && "anyOf" in item
? Array . isArray ( item . anyOf )
? item . anyOf
: [ item ]
: [ item ] ,
)
: value
const expanded =
key === "anyOf"
? flattened . flatMap ( ( item ) = > {
if ( typeof item !== "object" || item === null || ! ( "type" in item ) || ! ( "enum" in item ) ) return [ item ]
if ( Object . keys ( item ) . some ( ( property ) = > property !== "type" && property !== "enum" ) ) return [ item ]
if ( ! Array . isArray ( item . enum ) ) return [ item ]
return item . enum . map ( ( member ) = > ( { type : item . type , enum : [ member ] } ) )
} )
: flattened
const normalized = expanded . map ( ( item ) = > normalizeSchema ( item ) )
if ( key !== "anyOf" && key !== "required" && key !== "enum" ) return normalized
return [ . . . new Map ( normalized . map ( ( item ) = > [ JSON . stringify ( item ) , item ] ) ) . values ( ) ] . sort ( ( a , b ) = >
JSON . stringify ( a ) . localeCompare ( JSON . stringify ( b ) ) ,
)
}
if ( typeof value !== "object" || value === null ) return value
return Object . fromEntries (
Object . entries ( value )
. sort ( ( [ left ] , [ right ] ) = > left . localeCompare ( right ) )
. map ( ( [ property , child ] ) = > [ property , normalizeSchema ( child , property ) ] ) ,
)
}
2026-07-02 22:15:18 +00:00
function rewriteRefs ( value : unknown , renames : Map < string , string > ) : unknown {
if ( Array . isArray ( value ) ) return value . map ( ( item ) = > rewriteRefs ( item , renames ) )
if ( typeof value !== "object" || value === null ) return value
return Object . fromEntries (
Object . entries ( value ) . map ( ( [ key , child ] ) = > {
if ( key !== "$ref" || typeof child !== "string" ) return [ key , rewriteRefs ( child , renames ) ]
const prefix = "#/components/schemas/"
if ( ! child . startsWith ( prefix ) ) return [ key , child ]
return [ key , ` ${ prefix } ${ renames . get ( child . slice ( prefix . length ) ) ? ? child . slice ( prefix . length ) } ` ]
} ) ,
)
}
function inlineTypedAllOfConstraints ( value : unknown ) : void {
if ( Array . isArray ( value ) ) {
value . forEach ( inlineTypedAllOfConstraints )
return
}
if ( typeof value !== "object" || value === null ) return
const schema = value as { allOf? : unknown ; type ? : unknown ; [ key : string ] : unknown }
if ( typeof schema . type === "string" && Array . isArray ( schema . allOf ) && schema . allOf . every ( isConstraintSchema ) ) {
for ( const item of schema . allOf ) Object . assign ( schema , item )
delete schema . allOf
}
Object . values ( schema ) . forEach ( inlineTypedAllOfConstraints )
}
function isConstraintSchema ( value : unknown ) : value is Record < string , unknown > {
if ( typeof value !== "object" || value === null || Array . isArray ( value ) ) return false
2026-07-02 23:48:49 +00:00
return ! Object . keys ( value ) . some (
( key ) = > key === "$ref" || key === "type" || key === "allOf" || key === "anyOf" || key === "oneOf" ,
)
2026-07-02 22:15:18 +00:00
}