2026-05-05 12:01:30 +00:00
import { mkdir , readdir , readFile , rm , stat , writeFile } from 'node:fs/promises' ;
import { join } from 'node:path' ;
const componentsDir = process . env . HA _CORE _COMPONENTS _DIR || '/tmp/opencode/homeassistant-core/homeassistant/components' ;
const integrationsRoot = new URL ( '../ts/integrations/' , import . meta . url ) ;
const generatedRoot = new URL ( '../ts/integrations/generated/' , import . meta . url ) ;
const markerName = '.generated-by-smarthome-exchange' ;
const toClassName = ( domain ) => {
const parts = domain . split ( /[^a-zA-Z0-9]+/ ) . filter ( Boolean ) ;
const pascal = parts . map ( ( part ) => ` ${ part . slice ( 0 , 1 ) . toUpperCase ( ) } ${ part . slice ( 1 ) } ` ) . join ( '' ) || 'Unknown' ;
return ` HomeAssistant ${ pascal } Integration ` ;
} ;
const readManifest = async ( componentDir , fallbackDomain ) => {
try {
return JSON . parse ( await readFile ( join ( componentDir , 'manifest.json' ) , 'utf8' ) ) ;
} catch {
return { domain : fallbackDomain , name : fallbackDomain } ;
}
} ;
const isGeneratedFolder = async ( folderUrl ) => {
try {
await stat ( new URL ( ` ${ markerName } ` , folderUrl ) ) ;
return true ;
} catch {
return false ;
}
} ;
2026-05-05 20:05:48 +00:00
const fileExists = async ( fileUrl ) => {
try {
await stat ( fileUrl ) ;
return true ;
} catch {
return false ;
}
} ;
2026-05-05 12:01:30 +00:00
const json = ( value ) => JSON . stringify ( value , null , 2 ) ;
await mkdir ( integrationsRoot , { recursive : true } ) ;
await mkdir ( generatedRoot , { recursive : true } ) ;
for ( const entry of await readdir ( integrationsRoot , { withFileTypes : true } ) ) {
if ( ! entry . isDirectory ( ) ) continue ;
if ( entry . name === 'generated' ) continue ;
const folderUrl = new URL ( ` ./ ${ entry . name } / ` , integrationsRoot ) ;
if ( await isGeneratedFolder ( folderUrl ) ) {
await rm ( folderUrl , { recursive : true , force : true } ) ;
}
}
const ports = [ ] ;
for ( const entry of await readdir ( componentsDir , { withFileTypes : true } ) ) {
if ( ! entry . isDirectory ( ) ) continue ;
if ( entry . name . startsWith ( '__' ) ) continue ;
const componentDir = join ( componentsDir , entry . name ) ;
const manifest = await readManifest ( componentDir , entry . name ) ;
const domain = String ( manifest . domain || entry . name ) ;
const folderName = domain . replace ( /[^a-z0-9_]/gi , '_' ) . toLowerCase ( ) ;
const folderUrl = new URL ( ` ./ ${ folderName } / ` , integrationsRoot ) ;
const className = toClassName ( domain ) ;
const metadata = {
source : 'home-assistant/core' ,
upstreamPath : ` homeassistant/components/ ${ entry . name } ` ,
upstreamDomain : domain ,
integrationType : manifest . integration _type ? String ( manifest . integration _type ) : undefined ,
iotClass : manifest . iot _class ? String ( manifest . iot _class ) : undefined ,
qualityScale : manifest . quality _scale ? String ( manifest . quality _scale ) : undefined ,
requirements : Array . isArray ( manifest . requirements ) ? manifest . requirements . map ( String ) : [ ] ,
dependencies : Array . isArray ( manifest . dependencies ) ? manifest . dependencies . map ( String ) : [ ] ,
afterDependencies : Array . isArray ( manifest . after _dependencies ) ? manifest . after _dependencies . map ( String ) : [ ] ,
codeowners : Array . isArray ( manifest . codeowners ) ? manifest . codeowners . map ( String ) : [ ] ,
} ;
let handwritten = false ;
try {
await stat ( folderUrl ) ;
handwritten = ! ( await isGeneratedFolder ( folderUrl ) ) ;
} catch { }
if ( ! handwritten ) {
await mkdir ( folderUrl , { recursive : true } ) ;
await writeFile ( new URL ( markerName , folderUrl ) , 'This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.\n' ) ;
await writeFile (
new URL ( 'index.ts' , folderUrl ) ,
` export * from './ ${ folderName } .classes.integration.js'; \n export * from './ ${ folderName } .types.js'; \n `
) ;
await writeFile (
new URL ( ` ${ folderName } .types.ts ` , folderUrl ) ,
` export interface I ${ className . replace ( /Integration$/ , 'Config' ) } { \n // TODO: replace with the TypeScript-native config for ${ domain } . \n [key: string]: unknown; \n } \n `
) ;
await writeFile (
new URL ( ` ${ folderName } .classes.integration.ts ` , folderUrl ) ,
` import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; \n \n export class ${ className } extends DescriptorOnlyIntegration { \n constructor() { \n super({ \n domain: ${ JSON . stringify ( domain ) } , \n displayName: ${ JSON . stringify ( manifest . name ? String ( manifest . name ) : domain ) } , \n status: 'descriptor-only', \n metadata: ${ json ( metadata ) } , \n }); \n } \n } \n `
) ;
}
ports . push ( {
domain ,
folderName ,
className ,
handwritten ,
} ) ;
}
ports . sort ( ( a , b ) => a . domain . localeCompare ( b . domain ) ) ;
const imports = ports
. filter ( ( port ) => ! port . handwritten )
. map ( ( port ) => ` import { ${ port . className } } from '../ ${ port . folderName } /index.js'; ` )
. join ( '\n' ) ;
const constructorPushes = ports
. filter ( ( port ) => ! port . handwritten )
. map ( ( port ) => ` generatedHomeAssistantPortIntegrations.push(new ${ port . className } ()); ` )
. join ( '\n' ) ;
await writeFile (
new URL ( 'index.ts' , generatedRoot ) ,
` // Generated by scripts/generate-homeassistant-ports.mjs. Do not edit manually. \n \n import type { BaseIntegration } from '../../core/classes.baseintegration.js'; \n ${ imports } \n \n export const generatedHomeAssistantPortIntegrations: BaseIntegration[] = []; \n ${ constructorPushes } \n \n export const generatedHomeAssistantPortCount = ${ ports . filter ( ( port ) => ! port . handwritten ) . length } ; \n export const handwrittenHomeAssistantPortDomains = ${ json ( ports . filter ( ( port ) => port . handwritten ) . map ( ( port ) => port . domain ) ) } ; \n `
) ;
2026-05-05 20:05:48 +00:00
const handwrittenFolders = [ ] ;
for ( const entry of await readdir ( integrationsRoot , { withFileTypes : true } ) ) {
if ( ! entry . isDirectory ( ) ) continue ;
if ( entry . name === 'generated' ) continue ;
const folderUrl = new URL ( ` ./ ${ entry . name } / ` , integrationsRoot ) ;
if ( await isGeneratedFolder ( folderUrl ) ) continue ;
if ( ! ( await fileExists ( new URL ( 'index.ts' , folderUrl ) ) ) ) continue ;
handwrittenFolders . push ( entry . name ) ;
}
handwrittenFolders . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
await writeFile (
new URL ( 'index.ts' , integrationsRoot ) ,
[
'// Generated by scripts/generate-homeassistant-ports.mjs. Do not edit manually.' ,
"export * from './generated/index.js';" ,
... handwrittenFolders . map ( ( folderName ) => ` export * from './ ${ folderName } /index.js'; ` ) ,
'' ,
] . join ( '\n' )
) ;
2026-05-05 12:01:30 +00:00
console . log ( ` Generated ${ ports . filter ( ( port ) => ! port . handwritten ) . length } native TypeScript port skeletons. Preserved ${ ports . filter ( ( port ) => port . handwritten ) . length } handwritten folders. ` ) ;