11#!/usr/bin/env node
2- const crypto = require ( 'crypto' )
3- const { Command } = require ( 'commander' )
4- const { Doc : YDoc } = require ( 'yjs' )
5- const { WebrtcProvider } = require ( 'y-webrtc' )
6- const wrtc = require ( '@roamhq/wrtc' )
7- const fs = require ( 'fs' )
8- const path = require ( 'path' )
9- const readline = require ( 'readline' )
10- const chokidar = require ( 'chokidar' )
11- const { execSync } = require ( 'child_process' )
12- const chalk = require ( 'chalk' )
2+ import * as crypto from 'crypto'
3+ import { Command } from 'commander'
4+ import { Doc as YDoc } from 'yjs'
5+ import { WebrtcProvider } from 'y-webrtc'
6+ import * as wrtc from '@roamhq/wrtc'
7+ import * as fs from 'fs'
8+ import * as path from 'path'
9+ import * as readline from 'readline'
10+ import * as chokidar from 'chokidar'
11+ import { execSync } from 'child_process'
12+ import chalk from 'chalk'
13+ import * as os from 'os'
14+
15+ interface Interface {
16+ name: string
17+ host: string
18+ file: string
19+ url ? : string
20+ ydoc ? : YDoc
21+ provider ? : WebrtcProvider
22+ }
23+
24+ interface Config {
25+ signalingServer: string
26+ globalConfigPath: string
27+ localConfigPath: string
28+ defaultDiffBaseBranch: string
29+ diff ? : string | boolean
30+ isGitRepo ? : boolean
31+ interfaces ?: Interface [ ]
32+ }
33+
34+ const packageJson = JSON . parse ( fs . readFileSync ( path . join ( __dirname , '../../package.json' ) , 'utf8' ) )
1335
1436const program = new Command ( )
15- const DEFAULT_CONFIG = {
37+ const DEFAULT_CONFIG : Config = {
1638 signalingServer : 'wss://signaling.ghostpipe.dev' ,
17- globalConfigPath : path . join ( require ( 'os' ) . homedir ( ) , '.config' , 'ghostpipe' , 'config.json' ) ,
39+ globalConfigPath : path . join ( os . homedir ( ) , '.config' , 'ghostpipe' , 'config.json' ) ,
1840 localConfigPath : 'ghostpipe.config.json' ,
1941 defaultDiffBaseBranch : 'main' ,
2042}
@@ -23,34 +45,34 @@ let VERBOSE = false
2345program
2446 . name ( 'ghostpipe' )
2547 . description ( 'Interfaces for your codebase' )
26- . version ( require ( '../package.json' ) . version )
48+ . version ( packageJson . version )
2749 . argument ( '[url]' , 'Interface URL' )
2850 . argument ( '[file]' , 'The file this interface can use' )
2951 . option ( '--verbose' , 'Enable verbose logging' )
3052 . option ( '--diff [branch]' , 'Base branch for diff comparison' )
3153
32- program . action ( async ( url , file , options ) => {
33- VERBOSE = options . verbose
54+ program . action ( async ( url : string | undefined , file : string | undefined , options : { verbose ?: boolean , diff ?: string | boolean } ) => {
55+ VERBOSE = options . verbose || false
3456 await main ( url , file , options )
3557} )
3658
37- const log = ( ...args ) => VERBOSE && console . log ( chalk . gray ( ...args ) )
38- log . error = ( ...args ) => VERBOSE && console . error ( chalk . red ( ...args ) )
39- log . warn = ( ...args ) => VERBOSE && console . warn ( chalk . yellow ( ...args ) )
59+ const log = ( ...args : any [ ] ) => VERBOSE && console . log ( chalk . gray ( ...args ) )
60+ log . error = ( ...args : any [ ] ) => VERBOSE && console . error ( chalk . red ( ...args ) )
61+ log . warn = ( ...args : any [ ] ) => VERBOSE && console . warn ( chalk . yellow ( ...args ) )
4062
41- const main = async ( url , file , { diff} ) => {
42- const config = await buildConfig ( { url, file, diff} )
63+ const main = async ( url : string | undefined , file : string | undefined , options : { diff ?: string | boolean } ) => {
64+ const config = await buildConfig ( { url, file, diff : options . diff } )
4365 . then ( validateConfig )
4466 . then ( connectInterfaces )
45- config . interfaces . forEach (
67+ config . interfaces ? .forEach (
4668 intf => console . log ( chalk . cyan ( `${ intf . name } : ` ) + chalk . underline ( intf . url ) )
4769 )
4870}
4971
50- const buildConfig = async ( { url, file, diff} ) => {
72+ const buildConfig = async ( { url, file, diff } : { url ?: string , file ?: string , diff ?: string | boolean } ) : Promise < Config > => {
5173 const globalConfig = readJson ( DEFAULT_CONFIG . globalConfigPath )
5274 const localConfig = readJson ( globalConfig ?. localConfigPath || DEFAULT_CONFIG . localConfigPath )
53- const inlineInterface = await prepareInlineInterface ( { url, file} )
75+ const inlineInterface = await prepareInlineInterface ( { url, file } )
5476 return {
5577 ...DEFAULT_CONFIG ,
5678 ...globalConfig ,
@@ -61,7 +83,7 @@ const buildConfig = async ({url, file, diff}) => {
6183 }
6284}
6385
64- const validateConfig = config => {
86+ const validateConfig = ( config : Config ) : Config => {
6587 if ( config . diff && ! config . isGitRepo ) {
6688 console . warn ( chalk . yellow ( 'Warning: --diff flag specified but current directory is not a git repository' ) )
6789 process . exit ( 0 )
@@ -83,34 +105,34 @@ const validateConfig = config => {
83105 return config
84106}
85107
86- const connectInterfaces = config => ( {
108+ const connectInterfaces = ( config : Config ) : Config => ( {
87109 ...config ,
88- interfaces : config . interfaces . map ( intf => connectInterface ( intf , config ) )
110+ interfaces : config . interfaces ? .map ( intf => connectInterface ( intf , config ) )
89111} )
90112
91- const connectInterface = ( intf , config ) => {
113+ const connectInterface = ( intf : Interface , config : Config ) : Interface => {
92114 const connectedInterface = connectInterfaceToYjs ( intf , config )
93115 watchLocalFile ( connectedInterface , config )
94116 return connectedInterface
95117}
96118
97- const connectInterfaceToYjs = ( intf , config ) => {
119+ const connectInterfaceToYjs = ( intf : Interface , config : Config ) : Interface => {
98120 const pipe = crypto . randomBytes ( 16 ) . toString ( 'hex' )
99- const params = new URLSearchParams ( { pipe, signaling : config . signalingServer } )
121+ const params = new URLSearchParams ( { pipe, signaling : config . signalingServer } )
100122 const ydoc = new YDoc ( )
101- const provider = new WebrtcProvider ( pipe , ydoc , { signaling : [ config . signalingServer ] , peerOpts : { wrtc } } )
123+ const provider = new WebrtcProvider ( pipe , ydoc , { signaling : [ config . signalingServer ] , peerOpts : { wrtc } } )
102124 const meta = ydoc . getMap ( 'meta' )
103125 if ( config . isGitRepo ) {
104- meta . set ( 'base-branch' , config . diff )
126+ meta. set ( 'base-branch' , config . diff as string )
105127 meta . set ( 'head-branch' , getHeadBranch ( ) )
106128 }
107129 ydoc . getMap ( 'data' ) . observe ( ( event , transaction ) => {
108- log ( intf . name , 'transaction origin:' , transaction . origin ?. peerId || 'local' )
109- if ( ! transaction . origin ?. peerId ) return
130+ log ( intf . name , 'transaction origin:' , ( transaction . origin as any ) ?. peerId || 'local' )
131+ if ( ! ( transaction . origin as any ) ?. peerId ) return
110132 event . changes . keys . forEach ( ( change , key ) => {
111133 if ( change . action === 'update' || change . action === 'add' ) {
112134 log ( intf . name , 'ydoc event' , change . action , key )
113- debouncedWriteFile ( intf . file , ydoc , intf , config . diff )
135+ debouncedWriteFile ( intf . file , ydoc , intf , config . diff as string )
114136 } else if ( change . action === 'delete' ) {
115137 log ( 'TODO: handle delete file' )
116138 }
@@ -124,28 +146,28 @@ const connectInterfaceToYjs = (intf, config) => {
124146 }
125147}
126148
127- const watchLocalFile = ( intf , config ) => {
149+ const watchLocalFile = ( intf : Interface , config : Config ) : void => {
128150 const watcher = chokidar . watch ( [ intf . file ] , {
129151 persistent : true ,
130152 ignoreInitial : false
131153 } )
132154
133- watcher . on ( 'error' , error => {
134- console . error ( chalk . red ( 'ERROR:' ) , chalk . red ( error . message ) )
155+ watcher . on ( 'error' , ( error ) => {
156+ console . error ( chalk . red ( 'ERROR:' ) , chalk . red ( ( error as Error ) . message ) )
135157 process . exit ( 1 )
136158 } )
137159
138160 watcher . on ( 'all' , ( event , path ) => {
139161 if ( event === 'add' ) {
140- debouncedAdd ( path , intf , config . diff )
162+ debouncedAdd ( path , intf , config . diff as string )
141163 }
142164 if ( event === 'change' ) {
143- debouncedChange ( path , intf , config . diff )
165+ debouncedChange ( path , intf , config . diff as string )
144166 }
145167 } )
146168}
147169
148- const prepareInlineInterface = async ( { url, file} ) => {
170+ const prepareInlineInterface = async ( { url, file } : { url ? : string , file ?: string } ) : Promise < Interface | null > => {
149171 if ( ! url ) return null
150172 if ( ! file ) {
151173 file = await getFilePath ( `${ url . replace ( / ^ h t t p s ? : \/ \/ / , '' ) . replace ( / [ ^ a - z A - Z 0 - 9 . - ] / g, '_' ) } .txt` )
@@ -158,7 +180,7 @@ const prepareInlineInterface = async ({url, file}) => {
158180 }
159181}
160182
161- const createFileIfDoesNotExist = file => {
183+ const createFileIfDoesNotExist = ( file : string ) : void => {
162184 if ( ! fs . existsSync ( path . dirname ( file ) ) ) {
163185 fs . mkdirSync ( path . dirname ( file ) , { recursive : true } )
164186 }
@@ -167,7 +189,7 @@ const createFileIfDoesNotExist = file => {
167189 }
168190}
169191
170- const getFilePath = async ( defaultPath ) => {
192+ const getFilePath = async ( defaultPath : string ) : Promise < string > => {
171193 if ( fs . existsSync ( defaultPath ) )
172194 return defaultPath
173195
@@ -184,7 +206,7 @@ const getFilePath = async (defaultPath) => {
184206 } )
185207}
186208
187- const isGitRepo = ( ) => {
209+ const isGitRepo = ( ) : boolean => {
188210 try {
189211 execSync ( 'git rev-parse --git-dir' , { encoding : 'utf8' , stdio : 'pipe' } )
190212 return true
@@ -193,18 +215,18 @@ const isGitRepo = () => {
193215 }
194216}
195217
196- const addDiffFile = ( { intf, diff, file} ) => {
218+ const addDiffFile = ( { intf, diff, file } : { intf : Interface , diff : string , file : string } ) : void => {
197219 if ( ! diff || ! isGitRepo ( ) ) return
198220 try {
199- const content = execSync ( `git show ${ diff } :${ file } ` , { encoding : 'utf8' , stdio : 'pipe' } )
200- intf . ydoc . getMap ( 'base-data' ) . set ( 'content' , content )
221+ const content = execSync ( `git show ${ diff } :${ file } ` , { encoding : 'utf8' , stdio : 'pipe' } )
222+ intf . ydoc ? .getMap ( 'base-data' ) . set ( 'content' , content )
201223 log ( `Loaded diff file for ${ intf . name } : ${ file } ` )
202224 } catch ( error ) {
203225 log . error ( `File not in ${ diff } branch: ${ file } ` )
204226 }
205227}
206228
207- const readJson = ( file ) => {
229+ const readJson = ( file : string ) : any => {
208230 try {
209231 return JSON . parse ( fs . readFileSync ( file , 'utf8' ) )
210232 } catch ( error ) {
@@ -213,53 +235,54 @@ const readJson = (file) => {
213235 }
214236}
215237
216- const debounceByKey = ( func , delay ) => {
217- const timers = new Map ( )
218- return ( ...args ) => {
219- const key = args [ 0 ]
238+ const debounceByKey = < T extends ( ... args : any [ ] ) = > void > ( func : T , delay : number ) : T => {
239+ const timers = new Map < string , NodeJS . Timeout > ( )
240+ return ( ( ...args : Parameters < T > ) => {
241+ const key = args [ 0 ] as string
220242 clearTimeout ( timers . get ( key ) )
221243 timers . set ( key , setTimeout ( ( ) => {
222244 timers . delete ( key )
223- func . apply ( this , args )
245+ func . apply ( null , args )
224246 } , delay ) )
225- }
247+ } ) as T
226248}
227249
228- const debouncedAdd = debounceByKey ( ( path , intf , diff ) => {
250+ const debouncedAdd = debounceByKey ( ( path : string , intf : Interface , diff : string ) => {
229251 const content = fs . readFileSync ( path , 'utf8' )
230- intf . ydoc . transact ( ( ) => {
231- intf . ydoc . getMap ( 'data' ) . set ( 'content' , content )
252+ intf . ydoc ? .transact ( ( ) => {
253+ intf . ydoc ? .getMap ( 'data' ) . set ( 'content' , content )
232254 } )
233- addDiffFile ( { intf, diff, file : path } )
255+ addDiffFile ( { intf, diff, file : path } )
234256} , 300 )
235257
236- const debouncedChange = debounceByKey ( ( path , intf , diff ) => {
258+ const debouncedChange = debounceByKey ( ( path : string , intf : Interface , diff : string ) => {
237259 const fileContent = fs . readFileSync ( path , 'utf8' )
238- const content = intf . ydoc . getMap ( 'data' ) . get ( 'content' )
260+ const content = intf . ydoc ? .getMap ( 'data' ) . get ( 'content' )
239261 if ( content !== fileContent ) {
240262 log ( 'file change local' , path )
241- intf . ydoc . transact ( ( ) => {
242- intf . ydoc . getMap ( 'data' ) . set ( 'content' , fileContent )
263+ intf . ydoc ? .transact ( ( ) => {
264+ intf . ydoc ? .getMap ( 'data' ) . set ( 'content' , fileContent )
243265 } )
244266 }
245- addDiffFile ( { intf, diff, file : path } )
267+ addDiffFile ( { intf, diff, file : path } )
246268} , 300 )
247269
248- const debouncedWriteFile = debounceByKey ( ( key , ydoc , intf , diff ) => {
270+ const debouncedWriteFile = debounceByKey ( ( key : string , ydoc : YDoc , intf : Interface , diff : string ) => {
249271 const content = ydoc . getMap ( 'data' ) . get ( 'content' )
250272 const fileContent = fs . readFileSync ( key , 'utf8' )
251273 if ( content === fileContent ) return
252274 log ( intf . name , 'file change remote' , key )
253- fs . writeFileSync ( key , content , 'utf8' )
254- addDiffFile ( { diff, intf, file : key } )
275+ fs . writeFileSync ( key , content as string , 'utf8' )
276+ addDiffFile ( { diff, intf, file : key } )
255277} , 300 )
256278
257- const getHeadBranch = ( ) => {
279+ const getHeadBranch = ( ) : string | null = > {
258280 if ( ! isGitRepo ( ) ) return null
259281 try {
260282 return execSync ( 'git rev-parse --abbrev-ref HEAD' , { encoding : 'utf8' , stdio : 'pipe' } ) . trim ( )
261283 } catch ( error ) {
262- log . error ( 'Error getting branch:' , error . message )
284+ log . error ( 'Error getting branch:' , ( error as Error ) . message )
285+ return null
263286 }
264287}
265288
0 commit comments