@@ -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 } ` )
@@ -3117,10 +3170,7 @@ async function generateBlockDoc(blockPath: string) {
31173170 const displayType = stripVersionSuffix ( blockConfig . type )
31183171 const outputFilePath = path . join ( DOCS_OUTPUT_PATH , `${ displayType } .mdx` )
31193172
3120- let existingContent : string | null = null
3121- if ( fs . existsSync ( outputFilePath ) ) {
3122- existingContent = fs . readFileSync ( outputFilePath , 'utf-8' )
3123- }
3173+ const existingContent = readGeneratedFile ( outputFilePath )
31243174
31253175 const manualSections = existingContent ? extractManualContent ( existingContent ) : { }
31263176
@@ -3131,10 +3181,14 @@ async function generateBlockDoc(blockPath: string) {
31313181 finalContent = mergeWithManualContent ( markdown , existingContent , manualSections )
31323182 }
31333183
3134- fs . writeFileSync ( outputFilePath , finalContent )
3135- const logType =
3136- displayType !== blockConfig . type ? `${ displayType } (from ${ blockConfig . type } )` : displayType
3137- console . log ( `✓ Generated docs for ${ logType } ` )
3184+ emitGeneratedFile ( outputFilePath , finalContent )
3185+ if ( ! CHECK_ONLY ) {
3186+ const logType =
3187+ displayType !== blockConfig . type
3188+ ? `${ displayType } (from ${ blockConfig . type } )`
3189+ : displayType
3190+ console . log ( `✓ Generated docs for ${ logType } ` )
3191+ }
31383192 }
31393193 } catch ( error ) {
31403194 console . error ( `Error processing ${ blockPath } :` , error )
@@ -3376,6 +3430,13 @@ function cleanupStaleToolDocs(validToolDocs: Set<string>): void {
33763430 continue
33773431 }
33783432
3433+ if ( CHECK_ONLY ) {
3434+ staleArtifacts . push (
3435+ `${ path . relative ( rootDir , docPath ) } (stale page — regeneration would delete it)`
3436+ )
3437+ continue
3438+ }
3439+
33793440 fs . unlinkSync ( docPath )
33803441 console . log ( `✓ Removed stale tool doc: ${ blockType } .mdx` )
33813442 removedCount ++
@@ -3900,14 +3961,16 @@ async function generateAllTriggerDocs(): Promise<void> {
39003961 continue
39013962 }
39023963
3903- const existing = fs . existsSync ( outputFilePath )
3904- ? fs . readFileSync ( outputFilePath , 'utf-8' )
3905- : null
3964+ const existing = readGeneratedFile ( outputFilePath )
39063965
39073966 if ( existing ?. includes ( '\n## Actions' ) ) {
39083967 // Actions page generated this run by the block pass — append the Triggers section.
39093968 if ( ! existing . includes ( '\n## Triggers' ) ) {
3910- fs . appendFileSync ( outputFilePath , `\n${ buildTriggersSection ( triggers ) } ` )
3969+ if ( CHECK_ONLY ) {
3970+ emittedByPath . set ( outputFilePath , `${ existing } \n${ buildTriggersSection ( triggers ) } ` )
3971+ } else {
3972+ fs . appendFileSync ( outputFilePath , `\n${ buildTriggersSection ( triggers ) } ` )
3973+ }
39113974 }
39123975 } else {
39133976 // Trigger-only service (no actions block) — (re)write the standalone page,
@@ -3923,13 +3986,15 @@ async function generateAllTriggerDocs(): Promise<void> {
39233986 Object . keys ( manualSections ) . length > 0
39243987 ? mergeWithManualContent ( markdown , existing , manualSections )
39253988 : markdown
3926- fs . writeFileSync ( outputFilePath , finalContent )
3989+ emitGeneratedFile ( outputFilePath , finalContent )
39273990 }
39283991
39293992 generatedProviders . push ( blockType )
3930- console . log (
3931- `✓ Triggers for ${ formatTriggerProviderName ( provider ) } (${ triggers . length } trigger${ triggers . length === 1 ? '' : 's' } )`
3932- )
3993+ if ( ! CHECK_ONLY ) {
3994+ console . log (
3995+ `✓ Triggers for ${ formatTriggerProviderName ( provider ) } (${ triggers . length } trigger${ triggers . length === 1 ? '' : 's' } )`
3996+ )
3997+ }
39333998 }
39343999
39354000 console . log ( `✓ Trigger sections merged into ${ generatedProviders . length } integration pages` )
@@ -3990,21 +4055,37 @@ function updateMetaJson() {
39904055 pages : items ,
39914056 }
39924057
3993- fs . writeFileSync ( metaJsonPath , `${ JSON . stringify ( metaJson , null , 2 ) } \n` )
3994- console . log ( `Updated meta.json with ${ items . length } entries` )
4058+ emitGeneratedFile ( metaJsonPath , `${ JSON . stringify ( metaJson , null , 2 ) } \n` )
4059+ if ( ! CHECK_ONLY ) console . log ( `Updated meta.json with ${ items . length } entries` )
39954060}
39964061
39974062if ( import . meta. main ) {
3998- console . log ( 'Starting documentation generator...' )
4063+ CHECK_ONLY = process . argv . includes ( '--check' )
4064+ console . log (
4065+ CHECK_ONLY
4066+ ? 'Checking generated documentation freshness...'
4067+ : 'Starting documentation generator...'
4068+ )
39994069 generateAllBlockDocs ( )
40004070 . then ( ( success ) => {
4001- if ( success ) {
4002- console . log ( 'Documentation generation completed successfully' )
4003- process . exit ( 0 )
4004- } else {
4071+ if ( ! success ) {
40054072 console . error ( 'Documentation generation failed' )
40064073 process . exit ( 1 )
40074074 }
4075+ if ( CHECK_ONLY ) {
4076+ const stale = [ ...collectStaleEmissions ( ) , ...staleArtifacts ]
4077+ if ( stale . length > 0 ) {
4078+ console . error (
4079+ `Generated integration docs are stale:\n- ${ stale . join ( '\n- ' ) } \n` +
4080+ 'Run `bun run scripts/generate-docs.ts` and commit the result.'
4081+ )
4082+ process . exit ( 1 )
4083+ }
4084+ console . log ( '✓ Generated integration docs are in sync' )
4085+ process . exit ( 0 )
4086+ }
4087+ console . log ( 'Documentation generation completed successfully' )
4088+ process . exit ( 0 )
40084089 } )
40094090 . catch ( ( error ) => {
40104091 console . error ( 'Fatal error:' , error )
0 commit comments