@@ -358,23 +358,71 @@ interface IconRef {
358358 source : string
359359}
360360
361+ /**
362+ * Check mode (`--check`): render every generated artifact in memory and compare
363+ * it against the committed file instead of writing, so CI can fail on docs
364+ * drift the same way `tool-metadata:check` fails on stale tool metadata. Check
365+ * mode performs no filesystem mutations.
366+ *
367+ * The pipeline writes some pages twice per run — the block pass writes the base
368+ * page, then the trigger pass reads it back and appends/merges the Triggers
369+ * section — so check mode keeps an in-memory overlay of everything "written"
370+ * this run (`emittedByPath`), readers consult the overlay before disk
371+ * (`readGeneratedFile`), and staleness is judged once at the end against each
372+ * artifact's FINAL content. Comparing at emit time would flag the intermediate
373+ * block-pass content of every trigger-owning page as a false positive.
374+ *
375+ * Known limitation: `updateMetaJson` derives the sidebar from the mdx files on
376+ * disk, so in check mode a brand-new block's missing page is reported directly
377+ * while the corresponding meta.json entry is not — regenerating fixes both.
378+ */
379+ let CHECK_ONLY = false
380+ const staleArtifacts : string [ ] = [ ]
381+ const emittedByPath = new Map < string , string > ( )
382+
383+ /** Writes a generated artifact, or in check mode records its final content for the end-of-run comparison. */
384+ function emitGeneratedFile ( filePath : string , content : string ) : void {
385+ if ( CHECK_ONLY ) {
386+ emittedByPath . set ( filePath , content )
387+ return
388+ }
389+ fs . writeFileSync ( filePath , content )
390+ }
391+
392+ /** Reads a generated artifact as the pipeline would see it mid-run: overlay first in check mode, then disk. */
393+ function readGeneratedFile ( filePath : string ) : string | null {
394+ const emitted = emittedByPath . get ( filePath )
395+ if ( emitted !== undefined ) return emitted
396+ return fs . existsSync ( filePath ) ? fs . readFileSync ( filePath , 'utf-8' ) : null
397+ }
398+
399+ /** Compares every overlay entry against the committed file; returns repo-relative stale paths. */
400+ function collectStaleEmissions ( ) : string [ ] {
401+ const stale : string [ ] = [ ]
402+ for ( const [ filePath , content ] of emittedByPath ) {
403+ const committed = fs . existsSync ( filePath ) ? fs . readFileSync ( filePath , 'utf-8' ) : null
404+ if ( committed !== content ) stale . push ( path . relative ( rootDir , filePath ) )
405+ }
406+ return stale
407+ }
408+
361409/**
362410 * Copy the icons.tsx file from the main sim app to the docs app
363411 * This ensures icons are rendered consistently across both apps
364412 */
365413function copyIconsFile ( ) : void {
366414 try {
367- console . log ( 'Copying icons from sim app to docs app...' )
415+ if ( ! CHECK_ONLY ) console . log ( 'Copying icons from sim app to docs app...' )
368416
369417 if ( ! fs . existsSync ( ICONS_PATH ) ) {
370418 console . error ( `Source icons file not found: ${ ICONS_PATH } ` )
371419 return
372420 }
373421
374422 const iconsContent = fs . readFileSync ( ICONS_PATH , 'utf-8' )
375- fs . writeFileSync ( DOCS_ICONS_PATH , iconsContent )
423+ emitGeneratedFile ( DOCS_ICONS_PATH , iconsContent )
376424
377- console . log ( '✓ Icons successfully copied to docs app' )
425+ if ( ! CHECK_ONLY ) console . log ( '✓ Icons successfully copied to docs app' )
378426 } catch ( error ) {
379427 console . error ( 'Error copying icons file:' , error )
380428 }
@@ -579,8 +627,8 @@ ${mappingEntries}
579627}
580628`
581629
582- fs . writeFileSync ( iconMappingPath , content )
583- console . log ( '✓ Icon mapping file written to docs app' )
630+ emitGeneratedFile ( iconMappingPath , content )
631+ if ( ! CHECK_ONLY ) console . log ( '✓ Icon mapping file written to docs app' )
584632 } catch ( error ) {
585633 console . error ( 'Error writing icon mapping:' , error )
586634 }
@@ -938,8 +986,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
938986${ mappingEntries }
939987}
940988`
941- fs . writeFileSync ( iconMappingPath , content )
942- console . log ( '✓ Integration icon mapping written' )
989+ emitGeneratedFile ( iconMappingPath , content )
990+ if ( ! CHECK_ONLY ) console . log ( '✓ Integration icon mapping written' )
943991 } catch ( error ) {
944992 console . error ( 'Error writing integration icon mapping:' , error )
945993 }
@@ -1122,6 +1170,11 @@ async function writeIntegrationsJson(iconMapping: Record<string, IconRef>): Prom
11221170 return
11231171 }
11241172
1173+ if ( CHECK_ONLY ) {
1174+ staleArtifacts . push ( path . relative ( rootDir , jsonPath ) )
1175+ return
1176+ }
1177+
11251178 const updatedAt = new Date ( ) . toISOString ( ) . slice ( 0 , 10 )
11261179 fs . writeFileSync ( jsonPath , `${ serialize ( { updatedAt, integrations } ) } \n` )
11271180 console . log ( `✓ Integration data written: ${ integrations . length } integrations → ${ jsonPath } ` )
@@ -3041,10 +3094,7 @@ async function generateBlockDoc(blockPath: string) {
30413094 const displayType = stripVersionSuffix ( blockConfig . type )
30423095 const outputFilePath = path . join ( DOCS_OUTPUT_PATH , `${ displayType } .mdx` )
30433096
3044- let existingContent : string | null = null
3045- if ( fs . existsSync ( outputFilePath ) ) {
3046- existingContent = fs . readFileSync ( outputFilePath , 'utf-8' )
3047- }
3097+ const existingContent = readGeneratedFile ( outputFilePath )
30483098
30493099 const manualSections = existingContent ? extractManualContent ( existingContent ) : { }
30503100
@@ -3055,10 +3105,14 @@ async function generateBlockDoc(blockPath: string) {
30553105 finalContent = mergeWithManualContent ( markdown , existingContent , manualSections )
30563106 }
30573107
3058- fs . writeFileSync ( outputFilePath , finalContent )
3059- const logType =
3060- displayType !== blockConfig . type ? `${ displayType } (from ${ blockConfig . type } )` : displayType
3061- console . log ( `✓ Generated docs for ${ logType } ` )
3108+ emitGeneratedFile ( outputFilePath , finalContent )
3109+ if ( ! CHECK_ONLY ) {
3110+ const logType =
3111+ displayType !== blockConfig . type
3112+ ? `${ displayType } (from ${ blockConfig . type } )`
3113+ : displayType
3114+ console . log ( `✓ Generated docs for ${ logType } ` )
3115+ }
30623116 }
30633117 } catch ( error ) {
30643118 console . error ( `Error processing ${ blockPath } :` , error )
@@ -3300,6 +3354,13 @@ function cleanupStaleToolDocs(validToolDocs: Set<string>): void {
33003354 continue
33013355 }
33023356
3357+ if ( CHECK_ONLY ) {
3358+ staleArtifacts . push (
3359+ `${ path . relative ( rootDir , docPath ) } (stale page — regeneration would delete it)`
3360+ )
3361+ continue
3362+ }
3363+
33033364 fs . unlinkSync ( docPath )
33043365 console . log ( `✓ Removed stale tool doc: ${ blockType } .mdx` )
33053366 removedCount ++
@@ -3824,14 +3885,16 @@ async function generateAllTriggerDocs(): Promise<void> {
38243885 continue
38253886 }
38263887
3827- const existing = fs . existsSync ( outputFilePath )
3828- ? fs . readFileSync ( outputFilePath , 'utf-8' )
3829- : null
3888+ const existing = readGeneratedFile ( outputFilePath )
38303889
38313890 if ( existing ?. includes ( '\n## Actions' ) ) {
38323891 // Actions page generated this run by the block pass — append the Triggers section.
38333892 if ( ! existing . includes ( '\n## Triggers' ) ) {
3834- fs . appendFileSync ( outputFilePath , `\n${ buildTriggersSection ( triggers ) } ` )
3893+ if ( CHECK_ONLY ) {
3894+ emittedByPath . set ( outputFilePath , `${ existing } \n${ buildTriggersSection ( triggers ) } ` )
3895+ } else {
3896+ fs . appendFileSync ( outputFilePath , `\n${ buildTriggersSection ( triggers ) } ` )
3897+ }
38353898 }
38363899 } else {
38373900 // Trigger-only service (no actions block) — (re)write the standalone page,
@@ -3847,13 +3910,15 @@ async function generateAllTriggerDocs(): Promise<void> {
38473910 Object . keys ( manualSections ) . length > 0
38483911 ? mergeWithManualContent ( markdown , existing , manualSections )
38493912 : markdown
3850- fs . writeFileSync ( outputFilePath , finalContent )
3913+ emitGeneratedFile ( outputFilePath , finalContent )
38513914 }
38523915
38533916 generatedProviders . push ( blockType )
3854- console . log (
3855- `✓ Triggers for ${ formatTriggerProviderName ( provider ) } (${ triggers . length } trigger${ triggers . length === 1 ? '' : 's' } )`
3856- )
3917+ if ( ! CHECK_ONLY ) {
3918+ console . log (
3919+ `✓ Triggers for ${ formatTriggerProviderName ( provider ) } (${ triggers . length } trigger${ triggers . length === 1 ? '' : 's' } )`
3920+ )
3921+ }
38573922 }
38583923
38593924 console . log ( `✓ Trigger sections merged into ${ generatedProviders . length } integration pages` )
@@ -3914,21 +3979,37 @@ function updateMetaJson() {
39143979 pages : items ,
39153980 }
39163981
3917- fs . writeFileSync ( metaJsonPath , `${ JSON . stringify ( metaJson , null , 2 ) } \n` )
3918- console . log ( `Updated meta.json with ${ items . length } entries` )
3982+ emitGeneratedFile ( metaJsonPath , `${ JSON . stringify ( metaJson , null , 2 ) } \n` )
3983+ if ( ! CHECK_ONLY ) console . log ( `Updated meta.json with ${ items . length } entries` )
39193984}
39203985
39213986if ( import . meta. main ) {
3922- console . log ( 'Starting documentation generator...' )
3987+ CHECK_ONLY = process . argv . includes ( '--check' )
3988+ console . log (
3989+ CHECK_ONLY
3990+ ? 'Checking generated documentation freshness...'
3991+ : 'Starting documentation generator...'
3992+ )
39233993 generateAllBlockDocs ( )
39243994 . then ( ( success ) => {
3925- if ( success ) {
3926- console . log ( 'Documentation generation completed successfully' )
3927- process . exit ( 0 )
3928- } else {
3995+ if ( ! success ) {
39293996 console . error ( 'Documentation generation failed' )
39303997 process . exit ( 1 )
39313998 }
3999+ if ( CHECK_ONLY ) {
4000+ const stale = [ ...collectStaleEmissions ( ) , ...staleArtifacts ]
4001+ if ( stale . length > 0 ) {
4002+ console . error (
4003+ `Generated integration docs are stale:\n- ${ stale . join ( '\n- ' ) } \n` +
4004+ 'Run `bun run scripts/generate-docs.ts` and commit the result.'
4005+ )
4006+ process . exit ( 1 )
4007+ }
4008+ console . log ( '✓ Generated integration docs are in sync' )
4009+ process . exit ( 0 )
4010+ }
4011+ console . log ( 'Documentation generation completed successfully' )
4012+ process . exit ( 0 )
39324013 } )
39334014 . catch ( ( error ) => {
39344015 console . error ( 'Fatal error:' , error )
0 commit comments