@stacksjs/rpx 0.11.2 → 0.11.4
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/README.md +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/{chunk-p95pk6sx.js → chunk-pbbtnqsx.js} +55 -55
- package/dist/src/index.js +1 -1
- package/dist/types.d.ts +7 -1
- package/package.json +4 -3
- 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 +1361 -0
- package/src/types.ts +84 -0
- package/src/utils.ts +127 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { startProxies as startProxiesFunc } from './start'
|
|
2
|
+
|
|
3
|
+
export { colors } from './colors'
|
|
4
|
+
|
|
5
|
+
export { config, config as defaultConfig } from './config'
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
addHosts,
|
|
9
|
+
checkHosts,
|
|
10
|
+
removeHosts,
|
|
11
|
+
} from './hosts'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
checkExistingCertificates,
|
|
15
|
+
cleanupCertificates,
|
|
16
|
+
forceTrustCertificate,
|
|
17
|
+
generateCertificate,
|
|
18
|
+
httpsConfig,
|
|
19
|
+
isCertTrusted,
|
|
20
|
+
loadSSLConfig,
|
|
21
|
+
} from './https'
|
|
22
|
+
|
|
23
|
+
export { DefaultPortManager, findAvailablePort, isPortInUse, portManager } from './port-manager'
|
|
24
|
+
|
|
25
|
+
export { cleanup } from './start'
|
|
26
|
+
|
|
27
|
+
export { startProxies, startProxy, startServer } from './start'
|
|
28
|
+
|
|
29
|
+
export * from './types'
|
|
30
|
+
|
|
31
|
+
export * from './utils'
|
|
32
|
+
|
|
33
|
+
export default startProxiesFunc
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const log: {
|
|
2
|
+
info: (...args: any[]) => void
|
|
3
|
+
success: (...args: any[]) => void
|
|
4
|
+
warn: (...args: any[]) => void
|
|
5
|
+
error: (...args: any[]) => void
|
|
6
|
+
debug: (...args: any[]) => void
|
|
7
|
+
log: (...args: any[]) => void
|
|
8
|
+
start: (...args: any[]) => void
|
|
9
|
+
box: (...args: any[]) => void
|
|
10
|
+
} = {
|
|
11
|
+
info: (...args: any[]) => console.log('[info]', ...args),
|
|
12
|
+
success: (...args: any[]) => console.log('[success]', ...args),
|
|
13
|
+
warn: (...args: any[]) => console.warn('[warn]', ...args),
|
|
14
|
+
error: (...args: any[]) => console.error('[error]', ...args),
|
|
15
|
+
debug: (...args: any[]) => console.debug('[debug]', ...args),
|
|
16
|
+
log: (...args: any[]) => console.log(...args),
|
|
17
|
+
start: (...args: any[]) => console.log('[start]', ...args),
|
|
18
|
+
box: (...args: any[]) => console.log('[box]', ...args),
|
|
19
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { PortManager } from './types'
|
|
2
|
+
import * as net from 'node:net'
|
|
3
|
+
import { debugLog } from './utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if a port is in use
|
|
7
|
+
*/
|
|
8
|
+
export function isPortInUse(port: number, hostname: string, verbose?: boolean): Promise<boolean> {
|
|
9
|
+
debugLog('port', `Checking if port ${port} is in use on ${hostname}`, verbose)
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const server = net.createServer()
|
|
12
|
+
|
|
13
|
+
// Add a timeout to ensure we don't hang indefinitely
|
|
14
|
+
const timeout = setTimeout(() => {
|
|
15
|
+
debugLog('port', `Checking port ${port} timed out, assuming it's in use`, verbose)
|
|
16
|
+
server.close()
|
|
17
|
+
resolve(true)
|
|
18
|
+
}, 3000) // 3 second timeout
|
|
19
|
+
|
|
20
|
+
server.once('error', (err: NodeJS.ErrnoException) => {
|
|
21
|
+
clearTimeout(timeout)
|
|
22
|
+
if (err.code === 'EADDRINUSE') {
|
|
23
|
+
debugLog('port', `Port ${port} is in use`, verbose)
|
|
24
|
+
resolve(true)
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Other errors should also be treated as port unavailable
|
|
28
|
+
debugLog('port', `Error checking port ${port}: ${err.message}`, verbose)
|
|
29
|
+
resolve(true)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
server.once('listening', () => {
|
|
34
|
+
clearTimeout(timeout)
|
|
35
|
+
debugLog('port', `Port ${port} is available`, verbose)
|
|
36
|
+
server.close()
|
|
37
|
+
resolve(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
server.listen(port, hostname)
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
clearTimeout(timeout)
|
|
45
|
+
debugLog('port', `Exception checking port ${port}: ${err}`, verbose)
|
|
46
|
+
resolve(true)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find next available port
|
|
53
|
+
*/
|
|
54
|
+
export async function findAvailablePort(
|
|
55
|
+
startPort: number,
|
|
56
|
+
hostname: string,
|
|
57
|
+
verbose?: boolean,
|
|
58
|
+
maxAttempts = 50,
|
|
59
|
+
): Promise<number> {
|
|
60
|
+
debugLog('port', `Finding available port starting from ${startPort} (max attempts: ${maxAttempts})`, verbose)
|
|
61
|
+
let port = startPort
|
|
62
|
+
let attempts = 0
|
|
63
|
+
|
|
64
|
+
while (attempts < maxAttempts) {
|
|
65
|
+
attempts++
|
|
66
|
+
const isInUse = await isPortInUse(port, hostname, verbose)
|
|
67
|
+
|
|
68
|
+
if (!isInUse) {
|
|
69
|
+
debugLog('port', `Found available port: ${port} after ${attempts} attempts`, verbose)
|
|
70
|
+
return port
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
debugLog('port', `Port ${port} is in use, trying ${port + 1} (attempt ${attempts}/${maxAttempts})`, verbose)
|
|
74
|
+
port++
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(`Unable to find available port after ${maxAttempts} attempts starting from ${startPort}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Test if a port is actually connectable
|
|
82
|
+
*/
|
|
83
|
+
export function testPortConnectivity(
|
|
84
|
+
port: number,
|
|
85
|
+
hostname: string,
|
|
86
|
+
timeout = 5000,
|
|
87
|
+
verbose?: boolean,
|
|
88
|
+
): Promise<boolean> {
|
|
89
|
+
debugLog('port', `Testing connection to ${hostname}:${port}`, verbose)
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const socket = net.connect({
|
|
92
|
+
host: hostname,
|
|
93
|
+
port,
|
|
94
|
+
timeout,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
socket.once('connect', () => {
|
|
98
|
+
debugLog('port', `Successfully connected to ${hostname}:${port}`, verbose)
|
|
99
|
+
socket.end()
|
|
100
|
+
resolve(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
socket.once('timeout', () => {
|
|
104
|
+
debugLog('port', `Connection to ${hostname}:${port} timed out`, verbose)
|
|
105
|
+
socket.destroy()
|
|
106
|
+
resolve(false)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
socket.once('error', (err) => {
|
|
110
|
+
debugLog('port', `Failed to connect to ${hostname}:${port}: ${err.message}`, verbose)
|
|
111
|
+
socket.destroy()
|
|
112
|
+
resolve(false)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class DefaultPortManager implements PortManager {
|
|
118
|
+
usedPorts: Set<number> = new Set()
|
|
119
|
+
private hostname: string
|
|
120
|
+
private verbose?: boolean
|
|
121
|
+
private maxRetries: number
|
|
122
|
+
|
|
123
|
+
constructor(hostname: string = '0.0.0.0', verbose?: boolean, maxRetries = 50) {
|
|
124
|
+
this.hostname = hostname
|
|
125
|
+
this.verbose = verbose
|
|
126
|
+
this.maxRetries = maxRetries
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getNextAvailablePort(startPort: number, testConnectivity = false): Promise<number> {
|
|
130
|
+
if (this.usedPorts.has(startPort)) {
|
|
131
|
+
// If we already have this port registered as used, find another one
|
|
132
|
+
return this.findNextAvailablePort(startPort + 1, testConnectivity)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const isInUse = await isPortInUse(startPort, this.hostname, this.verbose)
|
|
136
|
+
|
|
137
|
+
if (isInUse) {
|
|
138
|
+
return this.findNextAvailablePort(startPort + 1, testConnectivity)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If requested, test that we can actually connect to this port
|
|
142
|
+
if (testConnectivity) {
|
|
143
|
+
const isConnectable = await testPortConnectivity(startPort, this.hostname, 3000, this.verbose)
|
|
144
|
+
if (!isConnectable) {
|
|
145
|
+
debugLog('port', `Port ${startPort} is available but not connectable, trying next port`, this.verbose)
|
|
146
|
+
return this.findNextAvailablePort(startPort + 1, testConnectivity)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Port is available, register it
|
|
151
|
+
this.usedPorts.add(startPort)
|
|
152
|
+
return startPort
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async findNextAvailablePort(startPort: number, testConnectivity = false): Promise<number> {
|
|
156
|
+
const port = await findAvailablePort(startPort, this.hostname, this.verbose, this.maxRetries)
|
|
157
|
+
|
|
158
|
+
// If requested, test that we can actually connect to this port
|
|
159
|
+
if (testConnectivity) {
|
|
160
|
+
const isConnectable = await testPortConnectivity(port, this.hostname, 3000, this.verbose)
|
|
161
|
+
if (!isConnectable) {
|
|
162
|
+
// If the port isn't connectable, try the next one
|
|
163
|
+
if (port < startPort + this.maxRetries) {
|
|
164
|
+
return this.findNextAvailablePort(port + 1, testConnectivity)
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
throw new Error(`Unable to find a connectable port after ${this.maxRetries} attempts`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.usedPorts.add(port)
|
|
173
|
+
return port
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
releasePort(port: number): void {
|
|
177
|
+
debugLog('port', `Releasing port ${port}`, this.verbose)
|
|
178
|
+
this.usedPorts.delete(port)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Global port manager instance
|
|
183
|
+
export const portManager: DefaultPortManager = new DefaultPortManager()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process'
|
|
2
|
+
import type { StartOptions } from './types'
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import * as process from 'node:process'
|
|
5
|
+
import { log } from './logger'
|
|
6
|
+
import { debugLog } from './utils'
|
|
7
|
+
|
|
8
|
+
export interface ManagedProcess {
|
|
9
|
+
command: string
|
|
10
|
+
cwd: string
|
|
11
|
+
process: ChildProcess | null
|
|
12
|
+
env?: Record<string, string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ProcessManager {
|
|
16
|
+
private processes: Map<string, ManagedProcess> = new Map()
|
|
17
|
+
private isShuttingDown = false
|
|
18
|
+
|
|
19
|
+
async startProcess(id: string, options: StartOptions, verbose?: boolean): Promise<void> {
|
|
20
|
+
if (this.processes.has(id)) {
|
|
21
|
+
debugLog('start', `Process ${id} is already running`, verbose)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const [cmd, ...args] = options.command.split(' ')
|
|
26
|
+
const cwd = options.cwd || process.cwd()
|
|
27
|
+
|
|
28
|
+
debugLog('start', `Starting process ${id}:`, verbose)
|
|
29
|
+
debugLog('start', ` Command: ${cmd} ${args.join(' ')}`, verbose)
|
|
30
|
+
debugLog('start', ` Working directory: ${cwd}`, verbose)
|
|
31
|
+
debugLog('start', ` Environment variables: ${JSON.stringify(options.env)}`, verbose)
|
|
32
|
+
|
|
33
|
+
const childProcess = spawn(cmd, args, {
|
|
34
|
+
cwd,
|
|
35
|
+
env: { ...process.env, ...options.env },
|
|
36
|
+
shell: true,
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
this.processes.set(id, {
|
|
41
|
+
command: options.command,
|
|
42
|
+
cwd,
|
|
43
|
+
process: childProcess,
|
|
44
|
+
env: options.env,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
childProcess.on('error', (err) => {
|
|
49
|
+
if (!this.isShuttingDown) {
|
|
50
|
+
debugLog('start', `Process ${id} failed to start: ${err}`, verbose)
|
|
51
|
+
this.processes.delete(id)
|
|
52
|
+
reject(err)
|
|
53
|
+
// Trigger cleanup on process error
|
|
54
|
+
process.emit('SIGINT')
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
childProcess.on('exit', (code) => {
|
|
59
|
+
if (!this.isShuttingDown && code !== null && code !== 0) {
|
|
60
|
+
debugLog('start', `Process ${id} exited with code ${code}`, verbose)
|
|
61
|
+
this.processes.delete(id)
|
|
62
|
+
reject(new Error(`Process ${id} exited with code ${code}`))
|
|
63
|
+
// Trigger cleanup on non-zero exit
|
|
64
|
+
process.emit('SIGINT')
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (verbose) {
|
|
69
|
+
childProcess.stdout?.on('data', (data) => {
|
|
70
|
+
debugLog('process', `[${id}] ${data.toString().trim()}`, true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
childProcess.stderr?.on('data', (data) => {
|
|
74
|
+
debugLog('process', `[${id}] ERR: ${data.toString().trim()}`, true)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Resolve after a delay to allow the process to start
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
if (!this.isShuttingDown && childProcess.killed) {
|
|
81
|
+
this.processes.delete(id)
|
|
82
|
+
reject(new Error(`Process ${id} was killed during startup`))
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
debugLog('start', `Process ${id} started successfully`, verbose)
|
|
86
|
+
resolve()
|
|
87
|
+
}
|
|
88
|
+
}, 1000)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async stopProcess(id: string, verbose?: boolean): Promise<void> {
|
|
93
|
+
const managed = this.processes.get(id)
|
|
94
|
+
if (!managed?.process) {
|
|
95
|
+
debugLog('start', `No process found for ${id}`, verbose)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
debugLog('start', `Stopping process ${id}`, verbose)
|
|
100
|
+
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
if (!managed.process) {
|
|
103
|
+
resolve()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
managed.process.once('exit', () => {
|
|
108
|
+
this.processes.delete(id)
|
|
109
|
+
debugLog('start', `Process ${id} stopped`, verbose)
|
|
110
|
+
resolve()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
managed.process.kill('SIGTERM')
|
|
115
|
+
|
|
116
|
+
// Force kill after 3 seconds if process hasn't exited
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
if (managed.process) {
|
|
119
|
+
debugLog('start', `Force killing process ${id}`, verbose)
|
|
120
|
+
try {
|
|
121
|
+
managed.process.kill('SIGKILL')
|
|
122
|
+
}
|
|
123
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
124
|
+
catch (err) {
|
|
125
|
+
// Ignore errors during force kill
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}, 3000)
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
debugLog('start', `Error stopping process ${id}: ${err}`, verbose)
|
|
132
|
+
this.processes.delete(id)
|
|
133
|
+
resolve()
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async stopAll(verbose?: boolean): Promise<void> {
|
|
139
|
+
if (this.isShuttingDown) {
|
|
140
|
+
debugLog('start', 'Already shutting down, skipping duplicate stopAll call', verbose)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.isShuttingDown = true
|
|
145
|
+
debugLog('start', 'Stopping all processes', verbose)
|
|
146
|
+
|
|
147
|
+
const promises = Array.from(this.processes.keys()).map(id =>
|
|
148
|
+
this.stopProcess(id, verbose).catch((err) => {
|
|
149
|
+
log.error(`Failed to stop process ${id}:`, err)
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
await Promise.allSettled(promises)
|
|
154
|
+
this.processes.clear()
|
|
155
|
+
this.isShuttingDown = false
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
isRunning(id: string): boolean {
|
|
159
|
+
const managed = this.processes.get(id)
|
|
160
|
+
return !!managed?.process && !managed.process.killed
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const processManager: ProcessManager = new ProcessManager()
|