11#!/usr/bin/env node
22import { IpcMessageSchema , MessageStatus , ResourceSchema } from '@codifycli/schemas' ;
3+ import commonjs from '@rollup/plugin-commonjs' ;
4+ import json from '@rollup/plugin-json' ;
5+ import nodeResolve from '@rollup/plugin-node-resolve' ;
6+ import typescript from '@rollup/plugin-typescript' ;
37import { Ajv } from 'ajv' ;
48import mergeJsonSchemas from 'merge-json-schemas' ;
59import { fork } from 'node:child_process' ;
6- import fs from 'node:fs' ;
10+ import fs from 'node:fs/promises ' ;
711import path from 'node:path' ;
12+ import { rollup } from 'rollup' ;
13+
14+ const rollupConfig = {
15+ input : 'src/index.ts' ,
16+ output : {
17+ dir : 'dist' ,
18+ format : 'cjs' ,
19+ inlineDynamicImports : true ,
20+ } ,
21+ treeshake : true ,
22+ external : [ '@homebridge/node-pty-prebuilt-multiarch' ] ,
23+ plugins : [
24+ json ( ) ,
25+ nodeResolve ( { exportConditions : [ 'node' ] } ) ,
26+ typescript ( {
27+ exclude : [ '**/*.test.ts' , '**/*.d.ts' , 'test' , 'bin' ]
28+ } ) ,
29+ commonjs ( ) ,
30+ // terser()
31+ ]
32+ } ;
833
9- import { SequentialPty , VerbosityLevel } from '../dist/index.js' ;
1034
1135const ajv = new Ajv ( {
1236 strict : true
1337} ) ;
1438const ipcMessageValidator = ajv . compile ( IpcMessageSchema ) ;
1539
40+ async function rollupProject ( ) {
41+ await fs . mkdir ( './dist' , { recursive : true } ) ;
42+
43+ const bundle = await rollup ( rollupConfig ) ;
44+ const { output } = await bundle . generate ( { dir : 'dist' , format : 'es' } )
45+
46+ for ( const a of output ) {
47+ if ( a . type !== 'asset' ) {
48+ await fs . writeFile ( path . join ( 'dist' , a . fileName ) , a . code ) ;
49+ }
50+ }
51+
52+ await bundle . close ( ) ;
53+ }
54+
1655function sendMessageAndAwaitResponse ( process , message ) {
1756 return new Promise ( ( resolve , reject ) => {
1857 process . on ( 'message' , ( response ) => {
@@ -35,39 +74,50 @@ function sendMessageAndAwaitResponse(process, message) {
3574 } ) ;
3675}
3776
38- function fetchDocumentationMaps ( ) {
77+ async function buildDocumentation ( ) {
3978 console . log ( 'Building documentation...' ) ;
4079
4180 const results = new Map ( ) ;
4281 const resourcesPath = path . resolve ( process . cwd ( ) , 'src' , 'resources' ) ;
43- const resourcesDir = fs . readdirSync ( resourcesPath ) ;
44-
45- for ( const resource of resourcesDir ) {
46- const resourcePath = path . join ( resourcesPath , resource ) ;
47- if ( ! isDirectory ( resourcePath ) ) continue ;
4882
49- const contents = fs . readdirSync ( resourcePath ) ;
50- const isGroup = contents . some ( ( content ) => isDirectory ( path . join ( resourcePath , content ) ) ) ;
51- const isAllDir = contents . every ( ( content ) => isDirectory ( path . join ( resourcePath , content ) ) ) ;
52-
53- if ( isGroup && ! isAllDir ) {
54- throw new Error ( `Documentation groups must only contain directories. ${ resourcePath } does not` ) ;
55- }
56-
57- if ( ! isGroup ) {
58- if ( contents . includes ( 'README.md' ) ) {
59- results . set ( resource , resource ) ;
60- }
61- } else {
62- for ( const innerDir of contents ) {
63- const innerDirReadme = path . join ( resourcePath , innerDir , 'README.md' ) ;
64- if ( isFile ( innerDirReadme ) ) {
65- results . set ( innerDir , path . relative ( './src/resources' , path . join ( resourcePath , innerDir ) ) ) ;
83+ // Helper function to recursively find README files
84+ async function findReadmeFiles ( dir , relativePath = '' ) {
85+ const entries = await fs . readdir ( dir , { withFileTypes : true } ) ;
86+
87+ for ( const entry of entries ) {
88+ const fullPath = path . join ( dir , entry . name ) ;
89+ const currentRelativePath = relativePath ? path . join ( relativePath , entry . name ) : entry . name ;
90+
91+ if ( entry . isDirectory ( ) ) {
92+ // Recurse into subdirectories
93+ await findReadmeFiles ( fullPath , currentRelativePath ) ;
94+ } else if ( entry . isFile ( ) && ( entry . name === 'README.md' || entry . name === 'README.mdx' ) ) {
95+ // Found a README file - determine its output path
96+ const sourceFile = path . join ( dir , entry . name ) ;
97+ const dirRelativePath = path . dirname ( currentRelativePath ) ;
98+
99+ let outputPath ;
100+ if ( relativePath === '' ) {
101+ // Root README.md in /src/resources -> /dist/docs/index.md
102+ outputPath = 'index.md' ;
103+ } else if ( dirRelativePath === '.' ) {
104+ // One level deep: /src/resources/git/README.md -> /dist/docs/resources/git.md
105+ outputPath = path . join ( 'resources' , path . basename ( currentRelativePath , path . extname ( currentRelativePath ) ) + '.md' ) ;
106+ } else {
107+ // Deeper nesting: maintain parent folders
108+ // /src/resources/package-managers/homebrew/README.md -> /dist/docs/resources/package-managers/homebrew.md
109+ const parentPath = path . dirname ( dirRelativePath ) ;
110+ const fileName = path . basename ( dirRelativePath ) ;
111+ outputPath = path . join ( 'resources' , parentPath , fileName + '.md' ) ;
66112 }
113+
114+ results . set ( sourceFile , outputPath ) ;
67115 }
68116 }
69117 }
70118
119+ await findReadmeFiles ( resourcesPath ) ;
120+
71121 return results ;
72122}
73123
@@ -87,103 +137,136 @@ function isFile(path) {
87137 }
88138}
89139
90- VerbosityLevel . set ( 3 ) ;
91- const $ = new SequentialPty ( ) ;
140+ async function main ( ) {
141+ await fs . rm ( './dist' , { recursive : true , force : true } ) ;
92142
93- await $ . spawn ( 'rm -rf ./dist')
94- await $ . spawn ( 'npm run rollup -- -f es' , { interactive : true } ) ;
143+ await fs . mkdir ( ' ./dist') ;
144+ await rollupProject ( ) ;
95145
96- const plugin = fork (
97- './dist/index.js' ,
98- [ ] ,
99- {
100- // Use default true to test plugins in secure mode (un-able to request sudo directly)
101- detached : true ,
102- env : { ...process . env } ,
103- execArgv : [ '--import' , 'tsx/esm' ] ,
104- } ,
105- )
106-
107- const initializeResult = await sendMessageAndAwaitResponse ( plugin , {
108- cmd : 'initialize' ,
109- data : { }
110- } )
111-
112- const { resourceDefinitions } = initializeResult ;
113- const resourceTypes = resourceDefinitions . map ( ( i ) => i . type ) ;
114- const resourceInfoMap = new Map ( ) ;
115-
116- const schemasMap = new Map ( )
117- for ( const type of resourceTypes ) {
118- const resourceInfo = await sendMessageAndAwaitResponse ( plugin , {
119- cmd : 'getResourceInfo' ,
120- data : { type }
121- } )
122-
123- schemasMap . set ( type , resourceInfo . schema ) ;
124- resourceInfoMap . set ( type , resourceInfo ) ;
125- }
146+ const plugin = fork (
147+ './dist/index.js' ,
148+ [ ] ,
149+ {
150+ // Use default true to test plugins in secure mode (un-able to request sudo directly)
151+ detached : true ,
152+ env : { ...process . env } ,
153+ execArgv : [ '--import' , 'tsx/esm' ] ,
154+ } ,
155+ )
126156
127- console . log ( resourceInfoMap ) ;
157+ try {
128158
129- const mergedSchemas = [ ...schemasMap . entries ( ) ] . map ( ( [ type , schema ] ) => {
130- // const resolvedSchema = await $RefParser.dereference(schema)
131- const resourceSchema = JSON . parse ( JSON . stringify ( ResourceSchema ) ) ;
159+ const initializeResult = await sendMessageAndAwaitResponse ( plugin , {
160+ cmd : 'initialize' ,
161+ data : { }
162+ } )
132163
133- delete resourceSchema . $id ;
134- delete resourceSchema . $schema ;
135- delete resourceSchema . title ;
136- delete resourceSchema . oneOf ;
137- delete resourceSchema . properties . type ;
164+ const { resourceDefinitions} = initializeResult ;
165+ const resourceTypes = resourceDefinitions . map ( ( i ) => i . type ) ;
166+ const resourceInfoMap = new Map ( ) ;
138167
139- if ( schema ) {
140- delete schema . $id ;
141- delete schema . $schema ;
142- delete schema . title ;
143- delete schema . oneOf ;
144- }
168+ const schemasMap = new Map ( )
169+ for ( const type of resourceTypes ) {
170+ const resourceInfo = await sendMessageAndAwaitResponse ( plugin , {
171+ cmd : 'getResourceInfo' ,
172+ data : { type }
173+ } )
145174
146- return mergeJsonSchemas ( [ schema ?? { } , resourceSchema , { properties : { type : { const : type , type : 'string' } } } ] ) ;
147- } ) ;
175+ schemasMap . set ( type , resourceInfo . schema ) ;
176+ resourceInfoMap . set ( type , resourceInfo ) ;
177+ }
178+
179+ console . log ( resourceInfoMap ) ;
180+
181+ const mergedSchemas = [ ...schemasMap . entries ( ) ] . map ( ( [ type , schema ] ) => {
182+ // const resolvedSchema = await $RefParser.dereference(schema)
183+ const resourceSchema = JSON . parse ( JSON . stringify ( ResourceSchema ) ) ;
184+
185+ delete resourceSchema . $id ;
186+ delete resourceSchema . $schema ;
187+ delete resourceSchema . title ;
188+ delete resourceSchema . oneOf ;
189+ delete resourceSchema . properties . type ;
190+
191+ if ( schema ) {
192+ delete schema . $id ;
193+ delete schema . $schema ;
194+ delete schema . title ;
195+ delete schema . oneOf ;
196+ }
197+
198+ return mergeJsonSchemas ( [ schema ?? { } , resourceSchema , { properties : { type : { const : type , type : 'string' } } } ] ) ;
199+ } ) ;
148200
149201
150- await $ . spawn ( 'rm -rf ./dist')
151- await $ . spawn ( 'npm run rollup' , { interactive : true } ) ; // re-run rollup without building for es
202+ await fs . rm ( ' ./dist', { recursive : true , force : true } ) ;
203+ await rollupProject ( ) ;
152204
153- console . log ( 'Generated JSON Schemas for all resources' )
205+ console . log ( 'Generated JSON Schemas for all resources' )
154206
155- const distFolder = path . resolve ( process . cwd ( ) , 'dist' ) ;
156- const schemaOutputPath = path . resolve ( distFolder , 'schemas.json' ) ;
157- fs . writeFileSync ( schemaOutputPath , JSON . stringify ( mergedSchemas , null , 2 ) ) ;
207+ const distFolder = path . resolve ( process . cwd ( ) , 'dist' ) ;
208+ const schemaOutputPath = path . resolve ( distFolder , 'schemas.json' ) ;
209+ await fs . writeFile ( schemaOutputPath , JSON . stringify ( mergedSchemas , null , 2 ) ) ;
158210
159- console . log ( 'Successfully wrote schema to ./dist/schemas.json' ) ;
211+ console . log ( 'Successfully wrote schema to ./dist/schemas.json' ) ;
160212
161- const documentationMap = fetchDocumentationMaps ( ) ;
213+ const documentationMap = await buildDocumentation ( ) ;
214+ console . log ( 'Documentation Map:' , documentationMap ) ;
162215
163- const packageJson = JSON . parse ( fs . readFileSync ( './package.json' , 'utf8' ) ) ;
216+ // Build reverse map for resource type -> documentation path
217+ const resourceTypeToDocPath = new Map ( ) ;
218+ for ( const [ sourceFile , outputPath ] of documentationMap . entries ( ) ) {
219+ // Extract resource type from source file path
220+ // e.g., /src/resources/git/README.md -> git
221+ // e.g., /src/resources/package-managers/homebrew/README.md -> homebrew
222+ const relativePath = path . relative ( path . resolve ( process . cwd ( ) , 'src' , 'resources' ) , sourceFile ) ;
223+ const parts = relativePath . split ( path . sep ) ;
164224
165- fs . writeFileSync ( './dist/manifest.json' , JSON . stringify ( {
166- name : packageJson . name ,
167- version : packageJson . version ,
168- description : packageJson . description ,
169- resources : [ ...resourceInfoMap . values ( ) ] . map ( ( info ) => ( {
170- type : info . type ,
171- description : info . description ?? info . schema ?. description ,
172- sensitiveParameters : info . sensitiveParameters ,
173- schema : info . schema ,
174- operatingSystems : info . operatingSystems ,
175- documentationKey : documentationMap . get ( info . type ) ,
176- } ) ) ,
177- } , null , 2 ) , 'utf8' ) ;
225+ // Remove README.md/README.mdx from the end
226+ parts . pop ( ) ;
178227
179- for ( const key of documentationMap . values ( ) ) {
180- fs . mkdirSync ( path . join ( 'dist' , 'documentation' , key ) , { recursive : true } )
228+ if ( parts . length > 0 ) {
229+ // Use the last directory name as the resource type
230+ const resourceType = parts [ parts . length - 1 ] ;
231+ resourceTypeToDocPath . set ( resourceType , outputPath ) ;
232+ }
233+ }
181234
182- fs . copyFileSync (
183- path . resolve ( path . join ( 'src' , 'resources' , key , 'README.md' ) ) ,
184- path . resolve ( path . join ( 'dist' , 'documentation' , key , 'README.md' ) ) ,
185- ) ;
235+ const packageJson = JSON . parse ( await fs . readFile ( './package.json' , 'utf8' ) ) ;
236+
237+ await fs . writeFile ( './dist/manifest.json' , JSON . stringify ( {
238+ name : packageJson . name ,
239+ version : packageJson . version ,
240+ description : packageJson . description ,
241+ resources : [ ...resourceInfoMap . values ( ) ] . map ( ( info ) => ( {
242+ type : info . type ,
243+ description : info . description ?? info . schema ?. description ,
244+ sensitiveParameters : info . sensitiveParameters ,
245+ schema : info . schema ,
246+ operatingSystems : info . operatingSystems ,
247+ documentationKey : resourceTypeToDocPath . get ( info . type ) ,
248+ } ) ) ,
249+ } , null , 2 ) , 'utf8' ) ;
250+
251+ // Copy documentation files to /dist/docs
252+ const docsPath = path . join ( 'dist' , 'docs' ) ;
253+ await fs . mkdir ( docsPath , { recursive : true } ) ;
254+
255+ for ( const [ sourceFile , outputPath ] of documentationMap . entries ( ) ) {
256+ const destFile = path . join ( docsPath , outputPath ) ;
257+ const destDir = path . dirname ( destFile ) ;
258+
259+ await fs . mkdir ( destDir , { recursive : true } ) ;
260+ await fs . copyFile ( sourceFile , destFile ) ;
261+
262+ console . log ( `Copied ${ sourceFile } -> ${ destFile } ` ) ;
263+ }
264+ } catch ( e ) {
265+ console . error ( e ) ;
266+ } finally {
267+ plugin . kill ( 9 ) ;
268+ process . exit ( 0 ) ;
269+ }
186270}
187271
188- plugin . kill ( 9 ) ;
189- process . exit ( 0 ) ;
272+ main ( ) ;
0 commit comments