@stacksjs/rpx 0.11.3 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +1 -1
- package/dist/{chunk-dz3837t8.js → chunk-8yenn1z8.js} +1 -1
- package/dist/chunk-cvt0dqrv.js +49 -0
- package/dist/{chunk-61re8msk.js → chunk-cy653fq8.js} +1 -1
- package/dist/chunk-grcvjvzg.js +124 -0
- package/dist/{chunk-gbny098p.js → chunk-sqn04kae.js} +1 -1
- package/dist/chunk-wcerh8e8.js +1 -0
- package/dist/https.d.ts +0 -13
- package/dist/process-manager.d.ts +1 -0
- package/dist/src/index.js +1 -1
- package/dist/start.d.ts +3 -0
- package/dist/utils.d.ts +11 -1
- package/package.json +3 -11
- package/src/colors.ts +13 -0
- package/src/config.ts +45 -0
- package/src/dns.ts +399 -0
- package/src/hosts.ts +257 -0
- package/src/https.ts +780 -0
- package/src/index.ts +33 -0
- package/src/logger.ts +19 -0
- package/src/port-manager.ts +183 -0
- package/src/process-manager.ts +164 -0
- package/src/start.ts +1357 -0
- package/src/types.ts +93 -0
- package/src/utils.ts +156 -0
- package/dist/chunk-8mnzvjyr.js +0 -123
- package/dist/chunk-94pvxvt5.js +0 -1
- package/dist/chunk-g5db14m7.js +0 -19
- /package/dist/{chunk-3y886wa5.js → chunk-hj5q1vd6.js} +0 -0
package/src/hosts.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { exec } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import * as process from 'node:process'
|
|
6
|
+
import { promisify } from 'node:util'
|
|
7
|
+
import { debugLog, getSudoPassword } from './utils'
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec)
|
|
10
|
+
|
|
11
|
+
export const hostsFilePath: string = process.platform === 'win32'
|
|
12
|
+
? path.join(process.env.windir || 'C:\\Windows', 'System32', 'drivers', 'etc', 'hosts')
|
|
13
|
+
: '/etc/hosts'
|
|
14
|
+
|
|
15
|
+
// Flag to track if we've already received sudo privileges in this session
|
|
16
|
+
let sudoPrivilegesAcquired = false
|
|
17
|
+
|
|
18
|
+
// Single function to execute sudo commands, with caching for permissions.
|
|
19
|
+
// Wraps in sh -c so pipes/redirects all run under sudo.
|
|
20
|
+
async function execSudo(command: string): Promise<string> {
|
|
21
|
+
if (process.platform === 'win32')
|
|
22
|
+
throw new Error('Administrator privileges required on Windows')
|
|
23
|
+
|
|
24
|
+
const sudoPassword = getSudoPassword()
|
|
25
|
+
const escaped = command.replace(/'/g, `'\\''`)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (sudoPassword) {
|
|
29
|
+
const { stdout } = await execAsync(`echo '${sudoPassword}' | sudo -S sh -c '${escaped}' 2>/dev/null`)
|
|
30
|
+
sudoPrivilegesAcquired = true
|
|
31
|
+
return stdout
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (sudoPrivilegesAcquired) {
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await execAsync(`sudo -n sh -c '${escaped}'`)
|
|
37
|
+
return stdout
|
|
38
|
+
}
|
|
39
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
40
|
+
catch (error) {
|
|
41
|
+
debugLog('hosts', 'Cached sudo privileges expired, requesting again', true)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { stdout } = await execAsync(`sudo sh -c '${escaped}'`)
|
|
46
|
+
sudoPrivilegesAcquired = true
|
|
47
|
+
return stdout
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new Error(`Failed to execute sudo command: ${(error as Error).message}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function addHosts(hosts: string[], verbose?: boolean): Promise<void> {
|
|
55
|
+
debugLog('hosts', `Adding hosts: ${hosts.join(', ')}`, verbose)
|
|
56
|
+
debugLog('hosts', `Using hosts file at: ${hostsFilePath}`, verbose)
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Read existing hosts file content
|
|
60
|
+
let existingContent: string
|
|
61
|
+
try {
|
|
62
|
+
existingContent = await fs.promises.readFile(hostsFilePath, 'utf-8')
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// /etc/hosts typically requires elevated permissions — fall back to sudo
|
|
66
|
+
debugLog('hosts', 'Reading hosts file requires elevated permissions, using sudo', verbose)
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
existingContent = await execSudo(`cat "${hostsFilePath}"`)
|
|
70
|
+
}
|
|
71
|
+
catch (sudoErr) {
|
|
72
|
+
console.log(' Could not read hosts file — skipping hosts setup')
|
|
73
|
+
debugLog('hosts', `sudo read also failed: ${sudoErr}`, verbose)
|
|
74
|
+
throw new Error(`Cannot read hosts file: ${sudoErr}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Prepare new entries, only including those that don't exist
|
|
79
|
+
const newEntries = hosts.filter((host) => {
|
|
80
|
+
const ipv4Entry = `127.0.0.1 ${host}`
|
|
81
|
+
const ipv6Entry = `::1 ${host}`
|
|
82
|
+
return !existingContent.includes(ipv4Entry) && !existingContent.includes(ipv6Entry)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (newEntries.length === 0) {
|
|
86
|
+
debugLog('hosts', 'All hosts already exist in hosts file', verbose)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create content for new entries
|
|
91
|
+
const hostEntries = newEntries.map(host =>
|
|
92
|
+
`\n# Added by rpx\n127.0.0.1 ${host}\n::1 ${host}`,
|
|
93
|
+
).join('\n')
|
|
94
|
+
|
|
95
|
+
const tmpFile = path.join(os.tmpdir(), `rpx-hosts-${Date.now()}.tmp`)
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Write to temporary file
|
|
99
|
+
await fs.promises.writeFile(tmpFile, existingContent + hostEntries, 'utf8')
|
|
100
|
+
|
|
101
|
+
// Use tee with sudo to write the content to hosts file
|
|
102
|
+
await execSudo(`cat "${tmpFile}" | tee "${hostsFilePath}" > /dev/null`)
|
|
103
|
+
console.log(` Hosts updated: ${newEntries.join(', ')}`)
|
|
104
|
+
}
|
|
105
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Don't throw — just tell the user what to add manually
|
|
108
|
+
console.log(' Could not update hosts file automatically')
|
|
109
|
+
console.log(' Add these entries to /etc/hosts:')
|
|
110
|
+
newEntries.forEach((host) => {
|
|
111
|
+
console.log(` 127.0.0.1 ${host}`)
|
|
112
|
+
console.log(` ::1 ${host}`)
|
|
113
|
+
})
|
|
114
|
+
console.log(` Or run: sudo nano ${hostsFilePath}`)
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
try {
|
|
118
|
+
await fs.promises.unlink(tmpFile)
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Ignore cleanup errors
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const error = err as Error
|
|
127
|
+
debugLog('hosts', `Failed to manage hosts file: ${error.message}`, verbose)
|
|
128
|
+
// Don't throw - hosts file management is best-effort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function removeHosts(hosts: string[], verbose?: boolean): Promise<void> {
|
|
133
|
+
debugLog('hosts', `Removing hosts: ${hosts.join(', ')}`, verbose)
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Read existing hosts file content
|
|
137
|
+
let content: string
|
|
138
|
+
try {
|
|
139
|
+
content = await fs.promises.readFile(hostsFilePath, 'utf-8')
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
debugLog('hosts', 'Reading hosts file requires elevated permissions, using sudo', verbose)
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
content = await execSudo(`cat "${hostsFilePath}"`)
|
|
146
|
+
}
|
|
147
|
+
catch (sudoErr) {
|
|
148
|
+
debugLog('hosts', `sudo read also failed: ${sudoErr}`, verbose)
|
|
149
|
+
throw new Error(`Cannot read hosts file: ${sudoErr}`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const lines = content.split('\n')
|
|
154
|
+
let modified = false
|
|
155
|
+
|
|
156
|
+
// Filter out our added entries and their comments
|
|
157
|
+
const filteredLines = lines.filter((line) => {
|
|
158
|
+
// Check if this line contains one of our hosts
|
|
159
|
+
const isHostLine = hosts.some(host =>
|
|
160
|
+
line.includes(` ${host}`)
|
|
161
|
+
&& (line.includes('127.0.0.1') || line.includes('::1')),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if (isHostLine) {
|
|
165
|
+
modified = true
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If it's our comment line, remove it
|
|
170
|
+
if (line.trim() === '# Added by rpx') {
|
|
171
|
+
modified = true
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return true
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// If nothing was removed, we're done
|
|
179
|
+
if (!modified) {
|
|
180
|
+
debugLog('hosts', 'No matching hosts found to remove', verbose)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Remove empty lines at the end of the file
|
|
185
|
+
while (filteredLines[filteredLines.length - 1]?.trim() === '')
|
|
186
|
+
filteredLines.pop()
|
|
187
|
+
|
|
188
|
+
// Ensure file ends with a single newline
|
|
189
|
+
const newContent = `${filteredLines.join('\n')}\n`
|
|
190
|
+
|
|
191
|
+
const tmpFile = path.join(os.tmpdir(), `rpx-hosts-${Date.now()}.tmp`)
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Write to temporary file
|
|
195
|
+
await fs.promises.writeFile(tmpFile, newContent, 'utf8')
|
|
196
|
+
|
|
197
|
+
// Use tee with sudo to write the content to hosts file
|
|
198
|
+
await execSudo(`cat "${tmpFile}" | tee "${hostsFilePath}" > /dev/null`)
|
|
199
|
+
debugLog('hosts', 'Hosts removed successfully', verbose)
|
|
200
|
+
}
|
|
201
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
202
|
+
catch (error) {
|
|
203
|
+
debugLog('hosts', 'Could not clean up hosts file automatically', verbose)
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
try {
|
|
207
|
+
// Clean up the temp file
|
|
208
|
+
await fs.promises.unlink(tmpFile)
|
|
209
|
+
}
|
|
210
|
+
catch (unlinkErr) {
|
|
211
|
+
// Ignore cleanup errors
|
|
212
|
+
debugLog('hosts', `Failed to remove temporary file: ${unlinkErr}`, verbose)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
debugLog('hosts', `Failed to clean up hosts file: ${(err as Error).message}`, verbose)
|
|
218
|
+
// Don't throw - hosts file cleanup is best-effort
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function checkHosts(hosts: string[], verbose?: boolean): Promise<boolean[]> {
|
|
223
|
+
debugLog('hosts', `Checking hosts: ${hosts}`, verbose)
|
|
224
|
+
|
|
225
|
+
let content: string
|
|
226
|
+
try {
|
|
227
|
+
content = await fs.promises.readFile(hostsFilePath, 'utf-8')
|
|
228
|
+
}
|
|
229
|
+
catch (readErr) {
|
|
230
|
+
debugLog('hosts', `Error reading hosts file: ${readErr}`, verbose)
|
|
231
|
+
|
|
232
|
+
// Try with sudo using SUDO_PASSWORD if available
|
|
233
|
+
try {
|
|
234
|
+
const sudoPassword = getSudoPassword()
|
|
235
|
+
let cmd: string
|
|
236
|
+
if (sudoPassword) {
|
|
237
|
+
cmd = `echo '${sudoPassword}' | sudo -S cat "${hostsFilePath}" 2>/dev/null`
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
cmd = `sudo -n cat "${hostsFilePath}" 2>/dev/null || cat "${hostsFilePath}" 2>/dev/null || echo ""`
|
|
241
|
+
}
|
|
242
|
+
const { stdout } = await execAsync(cmd)
|
|
243
|
+
content = stdout
|
|
244
|
+
}
|
|
245
|
+
catch (sudoErr) {
|
|
246
|
+
// Can't read hosts file - assume entries don't exist
|
|
247
|
+
debugLog('hosts', `Cannot read hosts file, assuming entries don't exist: ${sudoErr}`, verbose)
|
|
248
|
+
return hosts.map(() => false)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return hosts.map((host) => {
|
|
253
|
+
const ipv4Entry = `127.0.0.1 ${host}`
|
|
254
|
+
const ipv6Entry = `::1 ${host}`
|
|
255
|
+
return content.includes(ipv4Entry) || content.includes(ipv6Entry)
|
|
256
|
+
})
|
|
257
|
+
}
|