1+ import * as fs from 'fs' ;
2+ import * as os from 'os' ;
3+ import { spawn } from 'child_process' ;
4+
5+ export interface HostsEntry {
6+ ip : string ;
7+ domain : string ;
8+ comment ?: string ;
9+ }
10+
11+ /**
12+ * Simple logger interface to avoid circular dependency
13+ */
14+ interface Logger {
15+ info : ( message : string ) => void ;
16+ error : ( message : string ) => void ;
17+ }
18+
19+ // Simple console logger to avoid circular dependency
20+ const logger : Logger = {
21+ info : ( message : string ) => console . log ( `[INFO] ${ message } ` ) ,
22+ error : ( message : string ) => console . error ( `[ERROR] ${ message } ` )
23+ } ;
24+
25+ /**
26+ * Manages /etc/hosts file entries for custom domains
27+ */
28+ export class HostsManager {
29+ private readonly hostsPath : string ;
30+ private readonly marker = '# Flex Plugin Builder' ;
31+
32+ constructor ( ) {
33+ this . hostsPath = os . platform ( ) === 'win32' ?
34+ 'C:\\Windows\\System32\\drivers\\etc\\hosts' :
35+ '/etc/hosts' ;
36+ }
37+
38+ /**
39+ * Adds a domain mapping to /etc/hosts file
40+ * @param domain - The domain to map (e.g., 'flex.local.com')
41+ * @param ip - The IP address to map to (default: '127.0.0.1')
42+ */
43+ async addEntry ( domain : string , ip : string = '127.0.0.1' ) : Promise < void > {
44+ try {
45+ const content = await this . readHostsFile ( ) ;
46+ const lines = content . split ( '\n' ) ;
47+
48+ // Remove any existing entry for this domain
49+ const filteredLines = lines . filter ( line => {
50+ const trimmed = line . trim ( ) ;
51+ if ( trimmed . includes ( domain ) && trimmed . includes ( this . marker ) ) {
52+ return false ;
53+ }
54+ return true ;
55+ } ) ;
56+
57+ // Add new entry
58+ const newEntry = `${ ip } \t${ domain } \t${ this . marker } ` ;
59+ filteredLines . push ( newEntry ) ;
60+
61+ const newContent = filteredLines . join ( '\n' ) ;
62+ await this . writeHostsFile ( newContent ) ;
63+
64+ logger . info ( `✓ Added ${ domain } → ${ ip } to hosts file` ) ;
65+ } catch ( error ) {
66+ logger . error ( `Failed to update hosts file: ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
67+ throw new Error ( `Could not update hosts file. Please ensure you have necessary permissions.` ) ;
68+ }
69+ }
70+
71+ /**
72+ * Removes a domain mapping from /etc/hosts file
73+ * @param domain - The domain to remove
74+ */
75+ async removeEntry ( domain : string ) : Promise < void > {
76+ try {
77+ const content = await this . readHostsFile ( ) ;
78+ const lines = content . split ( '\n' ) ;
79+
80+ const filteredLines = lines . filter ( line => {
81+ const trimmed = line . trim ( ) ;
82+ if ( trimmed . includes ( domain ) && trimmed . includes ( this . marker ) ) {
83+ return false ;
84+ }
85+ return true ;
86+ } ) ;
87+
88+ const newContent = filteredLines . join ( '\n' ) ;
89+ await this . writeHostsFile ( newContent ) ;
90+
91+ logger . info ( `✓ Removed ${ domain } from hosts file` ) ;
92+ } catch ( error ) {
93+ logger . error ( `Failed to remove from hosts file: ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
94+ }
95+ }
96+
97+ /**
98+ * Checks if a domain is already mapped in /etc/hosts
99+ * @param domain - The domain to check
100+ */
101+ async hasEntry ( domain : string ) : Promise < boolean > {
102+ try {
103+ const content = await this . readHostsFile ( ) ;
104+ return content . includes ( domain ) ;
105+ } catch ( error ) {
106+ return false ;
107+ }
108+ }
109+
110+ /**
111+ * Removes all Flex Plugin Builder entries from hosts file
112+ */
113+ async cleanup ( ) : Promise < void > {
114+ try {
115+ const content = await this . readHostsFile ( ) ;
116+ const lines = content . split ( '\n' ) ;
117+
118+ const filteredLines = lines . filter ( line => {
119+ return ! line . includes ( this . marker ) ;
120+ } ) ;
121+
122+ const newContent = filteredLines . join ( '\n' ) ;
123+ await this . writeHostsFile ( newContent ) ;
124+
125+ logger . info ( '✓ Cleaned up all Flex Plugin Builder hosts entries' ) ;
126+ } catch ( error ) {
127+ logger . error ( `Failed to cleanup hosts file: ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
128+ }
129+ }
130+
131+ private async readHostsFile ( ) : Promise < string > {
132+ return new Promise ( ( resolve , reject ) => {
133+ fs . readFile ( this . hostsPath , 'utf8' , ( err , data ) => {
134+ if ( err ) {
135+ reject ( err ) ;
136+ } else {
137+ resolve ( data ) ;
138+ }
139+ } ) ;
140+ } ) ;
141+ }
142+
143+ private async writeHostsFile ( content : string ) : Promise < void > {
144+ return new Promise ( ( resolve , reject ) => {
145+ const isWindows = os . platform ( ) === 'win32' ;
146+
147+ if ( isWindows ) {
148+ // On Windows, try to run with elevated privileges
149+ const child = spawn ( 'powershell' , [
150+ '-Command' ,
151+ `Start-Process powershell -ArgumentList "-Command \\"Set-Content -Path '${ this . hostsPath } ' -Value @'${ content } '@\\"" -Verb RunAs -WindowStyle Hidden -Wait`
152+ ] , { stdio : 'inherit' } ) ;
153+
154+ child . on ( 'close' , ( code ) => {
155+ if ( code === 0 ) {
156+ resolve ( ) ;
157+ } else {
158+ reject ( new Error ( `PowerShell exited with code ${ code } ` ) ) ;
159+ }
160+ } ) ;
161+
162+ child . on ( 'error' , reject ) ;
163+ } else {
164+ // On Unix-like systems, use sudo
165+ const child = spawn ( 'sudo' , [ 'tee' , this . hostsPath ] , {
166+ stdio : [ 'pipe' , 'inherit' , 'inherit' ]
167+ } ) ;
168+
169+ child . stdin . write ( content ) ;
170+ child . stdin . end ( ) ;
171+
172+ child . on ( 'close' , ( code ) => {
173+ if ( code === 0 ) {
174+ resolve ( ) ;
175+ } else {
176+ reject ( new Error ( `sudo tee exited with code ${ code } ` ) ) ;
177+ }
178+ } ) ;
179+
180+ child . on ( 'error' , reject ) ;
181+ }
182+ } ) ;
183+ }
184+ }
185+
186+ export default HostsManager ;
0 commit comments