11#!/usr/bin/env node
22
33// Formats docs Markdown/MDX and repairs Mintlify accordion indentation.
4- import { execFileSync , spawnSync } from "node:child_process" ;
4+ import { spawnSync } from "node:child_process" ;
55import fs from "node:fs" ;
66import os from "node:os" ;
77import path from "node:path" ;
8+ import { pathToFileURL } from "node:url" ;
89import { repairMintlifyAccordionIndentation } from "./lib/mintlify-accordion.mjs" ;
10+ import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs" ;
911
1012const ROOT = path . resolve ( import . meta. dirname , ".." ) ;
1113const CHECK = process . argv . includes ( "--check" ) ;
12- const OXFMT_BIN = path . join ( ROOT , "node_modules" , "oxfmt" , "bin" , "oxfmt" ) ;
13- const OXFMT_CONFIG = path . join ( ROOT , ".oxfmtrc.jsonc" ) ;
14+ const DOCS_FORMAT_MAX_BUFFER_BYTES = 1024 * 1024 * 16 ;
15+ export const DOCS_FORMAT_MAX_COMMAND_LINE_BYTES = 24 * 1024 ;
16+ const FAILURE_OUTPUT_TAIL_BYTES = 16 * 1024 ;
1417
15- function docsFiles ( ) {
16- const output = execFileSync ( "git" , [ "ls-files" , "docs/**/*.md" , "docs/**/*.mdx" , "README.md" ] , {
17- cwd : ROOT ,
18+ function outputText ( value ) {
19+ if ( typeof value === "string" ) {
20+ return value ;
21+ }
22+ if ( Buffer . isBuffer ( value ) ) {
23+ return value . toString ( "utf8" ) ;
24+ }
25+ return "" ;
26+ }
27+
28+ function outputTail ( value ) {
29+ const text = outputText ( value ) . trim ( ) ;
30+ if ( ! text ) {
31+ return "" ;
32+ }
33+ const bytes = Buffer . from ( text , "utf8" ) ;
34+ if ( bytes . byteLength <= FAILURE_OUTPUT_TAIL_BYTES ) {
35+ return text ;
36+ }
37+ return bytes . subarray ( bytes . byteLength - FAILURE_OUTPUT_TAIL_BYTES ) . toString ( "utf8" ) ;
38+ }
39+
40+ function commandFailureMessage ( label , result , invocation ) {
41+ const details = [ ] ;
42+ if ( invocation ) {
43+ details . push ( `command: ${ invocation . command } ` ) ;
44+ if ( invocation . args . length > 0 ) {
45+ const previewArgs = invocation . args . slice ( 0 , 12 ) . join ( " " ) ;
46+ const suffix = invocation . args . length > 12 ? ` ... (${ invocation . args . length } args)` : "" ;
47+ details . push ( `args: ${ previewArgs } ${ suffix } ` ) ;
48+ }
49+ }
50+ if ( result . error ?. message ) {
51+ details . push ( result . error . message ) ;
52+ }
53+ if ( result . status !== null && result . status !== undefined && result . status !== 0 ) {
54+ details . push ( `exit status: ${ result . status } ` ) ;
55+ }
56+ if ( result . signal ) {
57+ details . push ( `signal: ${ result . signal } ` ) ;
58+ }
59+ const stderrTail = outputTail ( result . stderr ) ;
60+ if ( stderrTail ) {
61+ details . push ( `stderr tail:\n${ stderrTail } ` ) ;
62+ }
63+ const stdoutTail = outputTail ( result . stdout ) ;
64+ if ( stdoutTail ) {
65+ details . push ( `stdout tail:\n${ stdoutTail } ` ) ;
66+ }
67+ return `${ label } failed${ details . length > 0 ? `:\n${ details . join ( "\n" ) } ` : "" } ` ;
68+ }
69+
70+ export function docsFiles ( root = ROOT , deps = { } ) {
71+ const spawnSyncImpl = deps . spawnSync ?? spawnSync ;
72+ const result = spawnSyncImpl ( "git" , [ "ls-files" , "docs/**/*.md" , "docs/**/*.mdx" , "README.md" ] , {
73+ cwd : root ,
1874 encoding : "utf8" ,
75+ maxBuffer : DOCS_FORMAT_MAX_BUFFER_BYTES ,
1976 } ) ;
20- return output
77+ if ( result . error || result . status !== 0 ) {
78+ throw new Error (
79+ commandFailureMessage ( "git ls-files" , result , {
80+ command : "git" ,
81+ args : [ "ls-files" , "docs/**/*.md" , "docs/**/*.mdx" , "README.md" ] ,
82+ } ) ,
83+ ) ;
84+ }
85+ return outputText ( result . stdout )
2186 . split ( "\n" )
2287 . filter ( Boolean )
23- . filter ( ( relativePath ) => fs . existsSync ( path . join ( ROOT , relativePath ) ) ) ;
88+ . filter ( ( relativePath ) => ( deps . existsSync ?? fs . existsSync ) ( path . join ( root , relativePath ) ) ) ;
89+ }
90+
91+ function commandLineBytes ( args ) {
92+ return args . reduce ( ( total , arg ) => total + Buffer . byteLength ( arg , "utf8" ) + 3 , 0 ) ;
2493}
2594
26- function runOxfmt ( files ) {
27- const result = spawnSync (
28- process . execPath ,
29- [ OXFMT_BIN , "--write" , "--threads=1" , "--config" , OXFMT_CONFIG , ...files ] ,
30- {
31- cwd : ROOT ,
95+ export function chunkFilesForCommand (
96+ files ,
97+ prefixArgs ,
98+ maxBytes = DOCS_FORMAT_MAX_COMMAND_LINE_BYTES ,
99+ ) {
100+ const chunks = [ ] ;
101+ let chunk = [ ] ;
102+ let chunkBytes = commandLineBytes ( prefixArgs ) ;
103+
104+ for ( const file of files ) {
105+ const fileBytes = Buffer . byteLength ( file , "utf8" ) + 3 ;
106+ if ( chunk . length > 0 && chunkBytes + fileBytes > maxBytes ) {
107+ chunks . push ( chunk ) ;
108+ chunk = [ ] ;
109+ chunkBytes = commandLineBytes ( prefixArgs ) ;
110+ }
111+ chunk . push ( file ) ;
112+ chunkBytes += fileBytes ;
113+ }
114+
115+ if ( chunk . length > 0 ) {
116+ chunks . push ( chunk ) ;
117+ }
118+
119+ return chunks ;
120+ }
121+
122+ export function resolveOxfmtInvocation ( args , params = { } ) {
123+ const repoRoot = params . repoRoot ?? ROOT ;
124+ const platform = params . platform ?? process . platform ;
125+ const existsSync = params . existsSync ?? fs . existsSync ;
126+ const shimName = platform === "win32" ? "oxfmt.cmd" : "oxfmt" ;
127+ const shimPath = path . join ( repoRoot , "node_modules" , ".bin" , shimName ) ;
128+
129+ if ( existsSync ( shimPath ) ) {
130+ if ( platform === "win32" ) {
131+ const comSpec = params . comSpec ?? process . env . ComSpec ?? "cmd.exe" ;
132+ return {
133+ command : comSpec ,
134+ args : [ "/d" , "/s" , "/c" , buildCmdExeCommandLine ( shimPath , args ) ] ,
135+ shell : false ,
136+ windowsVerbatimArguments : true ,
137+ } ;
138+ }
139+ return {
140+ command : shimPath ,
141+ args,
142+ shell : false ,
143+ } ;
144+ }
145+
146+ return {
147+ command : params . nodeExecPath ?? process . execPath ,
148+ args : [ path . join ( repoRoot , "node_modules" , "oxfmt" , "bin" , "oxfmt" ) , ...args ] ,
149+ shell : false ,
150+ } ;
151+ }
152+
153+ export function runOxfmt ( files , params = { } , deps = { } ) {
154+ if ( files . length === 0 ) {
155+ return ;
156+ }
157+ const repoRoot = params . repoRoot ?? ROOT ;
158+ const spawnSyncImpl = deps . spawnSync ?? spawnSync ;
159+ const prefixArgs = [ "--write" , "--threads=1" , "--config" , path . join ( repoRoot , ".oxfmtrc.jsonc" ) ] ;
160+ for ( const chunk of chunkFilesForCommand (
161+ files ,
162+ prefixArgs ,
163+ params . maxCommandLineBytes ?? DOCS_FORMAT_MAX_COMMAND_LINE_BYTES ,
164+ ) ) {
165+ const invocation = resolveOxfmtInvocation ( [ ...prefixArgs , ...chunk ] , {
166+ comSpec : params . comSpec ,
167+ existsSync : deps . existsSync ,
168+ nodeExecPath : params . nodeExecPath ,
169+ platform : params . platform ,
170+ repoRoot,
171+ } ) ;
172+ const result = spawnSyncImpl ( invocation . command , invocation . args , {
173+ cwd : repoRoot ,
32174 encoding : "utf8" ,
33- maxBuffer : 1024 * 1024 * 16 ,
34- } ,
35- ) ;
175+ maxBuffer : DOCS_FORMAT_MAX_BUFFER_BYTES ,
176+ shell : invocation . shell ,
177+ windowsVerbatimArguments : invocation . windowsVerbatimArguments ,
178+ } ) ;
36179
37- if ( result . status !== 0 ) {
38- const stderr = result . stderr . trim ( ) ;
39- throw new Error ( `oxfmt failed ${ stderr ? `:\n ${ stderr } ` : "" } ` ) ;
180+ if ( result . error || result . status !== 0 ) {
181+ throw new Error ( commandFailureMessage ( "oxfmt" , result , invocation ) ) ;
182+ }
40183 }
41184}
42185
43- function repairFiles ( root , files ) {
186+ export function repairFiles ( root , files ) {
44187 const changed = [ ] ;
45188 for ( const relativePath of files ) {
46189 const absolutePath = path . join ( root , relativePath ) ;
@@ -55,50 +198,71 @@ function repairFiles(root, files) {
55198 return changed ;
56199}
57200
58- function copyDocsToTemp ( files ) {
201+ function copyDocsToTemp ( root , files ) {
59202 const tempRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-docs-format-" ) ) ;
60203 for ( const relativePath of files ) {
61- const source = path . join ( ROOT , relativePath ) ;
204+ const source = path . join ( root , relativePath ) ;
62205 const target = path . join ( tempRoot , relativePath ) ;
63206 fs . mkdirSync ( path . dirname ( target ) , { recursive : true } ) ;
64207 fs . copyFileSync ( source , target ) ;
65208 }
66209 return tempRoot ;
67210}
68211
69- const changed = [ ] ;
70- const files = docsFiles ( ) ;
71-
72- if ( CHECK ) {
73- const tempRoot = copyDocsToTemp ( files ) ;
74- try {
75- runOxfmt ( files . map ( ( relativePath ) => path . join ( tempRoot , relativePath ) ) ) ;
76- repairFiles ( tempRoot , files ) ;
77- for ( const relativePath of files ) {
78- const raw = fs . readFileSync ( path . join ( ROOT , relativePath ) , "utf8" ) ;
79- const formatted = fs . readFileSync ( path . join ( tempRoot , relativePath ) , "utf8" ) ;
80- if ( formatted !== raw ) {
81- changed . push ( relativePath ) ;
212+ export function formatDocs ( params = { } , deps = { } ) {
213+ const root = params . root ?? ROOT ;
214+ const check = params . check ?? false ;
215+ const changed = [ ] ;
216+ const files = docsFiles ( root , deps ) ;
217+
218+ if ( check ) {
219+ const tempRoot = copyDocsToTemp ( root , files ) ;
220+ try {
221+ runOxfmt (
222+ files . map ( ( relativePath ) => path . join ( tempRoot , relativePath ) ) ,
223+ { ...params , repoRoot : root } ,
224+ deps ,
225+ ) ;
226+ repairFiles ( tempRoot , files ) ;
227+ for ( const relativePath of files ) {
228+ const raw = fs . readFileSync ( path . join ( root , relativePath ) , "utf8" ) ;
229+ const formatted = fs . readFileSync ( path . join ( tempRoot , relativePath ) , "utf8" ) ;
230+ if ( formatted !== raw ) {
231+ changed . push ( relativePath ) ;
232+ }
82233 }
234+ } finally {
235+ fs . rmSync ( tempRoot , { recursive : true , force : true } ) ;
83236 }
84- } finally {
85- fs . rmSync ( tempRoot , { recursive : true , force : true } ) ;
237+ } else {
238+ runOxfmt ( files , { ...params , repoRoot : root } , deps ) ;
239+ changed . push ( ...repairFiles ( root , files ) ) ;
86240 }
87- } else {
88- runOxfmt ( files ) ;
89- changed . push ( ...repairFiles ( ROOT , files ) ) ;
241+
242+ return {
243+ changed,
244+ fileCount : files . length ,
245+ } ;
90246}
91247
92- if ( CHECK && changed . length > 0 ) {
93- console . error ( `Format issues found in ${ changed . length } docs file(s):` ) ;
94- for ( const relativePath of changed ) {
95- console . error ( `- ${ relativePath } ` ) ;
248+ function main ( ) {
249+ const { changed, fileCount } = formatDocs ( { check : CHECK , root : ROOT } ) ;
250+
251+ if ( CHECK && changed . length > 0 ) {
252+ console . error ( `Format issues found in ${ changed . length } docs file(s):` ) ;
253+ for ( const relativePath of changed ) {
254+ console . error ( `- ${ relativePath } ` ) ;
255+ }
256+ process . exit ( 1 ) ;
257+ }
258+
259+ if ( changed . length > 0 ) {
260+ console . log ( `Formatted ${ changed . length } docs file(s).` ) ;
261+ } else {
262+ console . log ( `Docs formatting clean (${ fileCount } files).` ) ;
96263 }
97- process . exit ( 1 ) ;
98264}
99265
100- if ( changed . length > 0 ) {
101- console . log ( `Formatted ${ changed . length } docs file(s).` ) ;
102- } else {
103- console . log ( `Docs formatting clean (${ files . length } files).` ) ;
266+ if ( import . meta. url === pathToFileURL ( process . argv [ 1 ] ?? "" ) . href ) {
267+ main ( ) ;
104268}
0 commit comments