11#!/usr/bin/env node
22
33import { createServer } from 'node:http'
4+ import { execFile } from 'node:child_process'
45import { readFile } from 'node:fs/promises'
56import { resolve } from 'node:path'
7+ import { setTimeout as wait } from 'node:timers/promises'
68import { fileURLToPath } from 'node:url'
9+ import { formatWithOptions , promisify } from 'node:util'
710
811const root = fileURLToPath ( new URL ( '..' , import . meta. url ) )
912
13+ const release = ( port , {
14+ timeout = 3000 ,
15+ poll : { interval = 50 } = { }
16+ } = { } ) =>
17+ Promise . resolve ( port )
18+ . then ( port => Number . isFinite ( port ) && port >= 1 && port <= 65535
19+ ? port
20+ : Promise . reject ( new RangeError ( `port: ${ port } , must be 1 - 65535` ) ) )
21+ . then ( port => promisify ( execFile ) ( 'lsof' , [ '-ti' , `tcp:${ port } ` , '-sTCP:LISTEN' ] ) )
22+ . then ( ( { stdout } ) => stdout . trim ( ) . split ( '\n' ) . map ( Number ) . filter ( Boolean ) )
23+ . then ( pids => pids . map ( pid => ( process . kill ( pid , 'SIGTERM' ) , pid ) ) )
24+ . then ( function poll ( pids , start = Date . now ( ) ) {
25+ const live = pids . filter ( pid => {
26+ try { return process . kill ( pid , 0 ) , true }
27+ catch ( err ) {
28+ if ( err . code === 'ESRCH' ) return false
29+ throw err
30+ }
31+ } )
32+
33+ return ! live . length ? pids :
34+ Date . now ( ) - start >= timeout
35+ ? ( live . forEach ( pid => process . kill ( pid , 'SIGKILL' ) ) , pids )
36+ : wait ( Math . max ( 10 , interval ) ) . then ( ( ) => poll ( pids , start ) )
37+ } )
38+ . catch ( err => err . code === 1 ? [ ] : Promise . reject ( err ) )
39+
40+ const log = ( ...args ) =>
41+ console . log ( formatWithOptions ( { colors : true } , ...args ) )
42+
1043const aliases = {
1144 '-p' : '--port' ,
1245 '-H' : '--host' ,
@@ -115,11 +148,15 @@ if ('help' in args) {
115148 const pathname = url . pathname
116149
117150 const request =
118- pathname === '/pointerdriver.js'
119- ? { file : 'index.js' }
120- : pathname . match ( / ^ \/ s r c \/ [ \w - ] + \/ i n d e x \. j s $ / )
121- ? { file : pathname . slice ( 1 ) }
122- : null
151+ pathname === '/'
152+ ? { file : 'bin/skill.md' , type : 'text/markdown; charset=utf-8' }
153+ : pathname === '/pointerdriver.js'
154+ ? { file : 'index.js' , type : 'text/javascript; charset=utf-8' }
155+ : pathname . match ( / ^ \/ s r c \/ [ \w - ] + \/ i n d e x \. j s $ / )
156+ ? { file : pathname . slice ( 1 ) , type : 'text/javascript; charset=utf-8' }
157+ : pathname . match ( / ^ \/ f o n t s \/ [ \w - ] + \. s v g $ / )
158+ ? { file : pathname . slice ( 1 ) , type : 'image/svg+xml' }
159+ : null
123160
124161 if ( ! request )
125162 return send ( req , res , 404 , {
@@ -128,11 +165,14 @@ if ('help' in args) {
128165 } , 'Not found' )
129166
130167 const path = resolve ( root , request . file )
131- const body = await js ( path )
168+
169+ const body = request . type . startsWith ( 'text/javascript' )
170+ ? await js ( path )
171+ : await readFile ( path , 'utf8' )
132172
133173 return send ( req , res , 200 , {
134174 ...cors ,
135- 'Content-Type' : 'text/javascript; charset=utf-8' ,
175+ 'Content-Type' : request . type ,
136176 'Cache-Control' : 'no-store' ,
137177 'X-Content-Type-Options' : 'nosniff' ,
138178 } , body )
@@ -157,10 +197,18 @@ if ('help' in args) {
157197 }
158198 } )
159199
200+ log ( 'starting server ...' )
201+ log ( 'checking port %d ...' , port )
202+
203+ const released = await release ( port )
204+
205+ if ( released . length )
206+ log ( 'killed: %s to free up port: %d' , released . join ( ', ' ) , port )
207+
160208 const logHost = host . includes ( ':' ) ? `[${ host } ]` : host
161209
162210 server . listen ( port , host , ( ) =>
163- console . log ( ` http://${ logHost } : ${ port } /pointerdriver.js` ) )
211+ log ( 'listening: http://%s:%d/' , logHost , port ) )
164212
165213 const shutdown = ( ) => server . close ( )
166214
0 commit comments