hermium 0.1.2 → 0.1.3
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/bin/hermium.mjs +181 -145
- package/dist/server/index.mjs +65 -65
- package/dist/web-server/index.mjs +611 -0
- package/package.json +1 -1
package/bin/hermium.mjs
CHANGED
|
@@ -7,44 +7,41 @@ import { homedir } from 'os'
|
|
|
7
7
|
import pc from 'picocolors'
|
|
8
8
|
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const pkgDir = resolve(__dirname, '..')
|
|
11
|
+
const apiEntry = resolve(pkgDir, 'dist', 'server', 'index.mjs')
|
|
12
|
+
const webEntry = resolve(pkgDir, 'dist', 'web-server', 'index.mjs')
|
|
13
|
+
const WEB_UI_HOME = resolve(homedir(), '.hermium')
|
|
14
|
+
const API_PID_FILE = join(WEB_UI_HOME, 'api.pid')
|
|
15
|
+
const WEB_PID_FILE = join(WEB_UI_HOME, 'web.pid')
|
|
16
|
+
const API_LOG_FILE = join(WEB_UI_HOME, 'api.log')
|
|
17
|
+
const WEB_LOG_FILE = join(WEB_UI_HOME, 'web.log')
|
|
18
|
+
const DEFAULT_API_PORT = 8788
|
|
19
|
+
const DEFAULT_WEB_PORT = 3000
|
|
10
20
|
|
|
11
21
|
// ─── Runtime detection ─────────────────────────────────────────────────────
|
|
12
22
|
|
|
13
|
-
function
|
|
14
|
-
// Check if bun is available on PATH
|
|
23
|
+
function getRuntimeCmd() {
|
|
15
24
|
try {
|
|
16
25
|
execSync('bun --version', { stdio: 'ignore' })
|
|
17
|
-
return
|
|
26
|
+
return 'bun'
|
|
18
27
|
} catch {
|
|
19
|
-
// fall back to current process
|
|
20
28
|
const exe = process.execPath
|
|
21
29
|
const isBun = exe.includes('bun') || (process.versions && process.versions.bun)
|
|
22
|
-
|
|
30
|
+
if (!isBun) {
|
|
31
|
+
console.log(pc.red(' ✗ Bun is required to run Hermium.'))
|
|
32
|
+
console.log(' Install: curl -fsSL https://bun.sh/install | bash')
|
|
33
|
+
console.log(' Or: npm install -g bun')
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
return exe
|
|
23
37
|
}
|
|
24
38
|
}
|
|
25
39
|
|
|
26
|
-
function getRuntimeCmd() {
|
|
27
|
-
const rt = detectRuntime()
|
|
28
|
-
if (!rt.found) {
|
|
29
|
-
console.log(pc.red(' ✗ Bun is required to run Hermium.'))
|
|
30
|
-
console.log(' Install: curl -fsSL https://bun.sh/install | bash')
|
|
31
|
-
console.log(' Or: npm install -g bun')
|
|
32
|
-
process.exit(1)
|
|
33
|
-
}
|
|
34
|
-
return rt.cmd
|
|
35
|
-
}
|
|
36
|
-
const pkgDir = resolve(__dirname, '..')
|
|
37
|
-
const serverEntry = resolve(pkgDir, 'dist', 'server', 'index.mjs')
|
|
38
|
-
const WEB_UI_HOME = resolve(homedir(), '.hermium')
|
|
39
|
-
const PID_FILE = join(WEB_UI_HOME, 'server.pid')
|
|
40
|
-
const LOG_FILE = join(WEB_UI_HOME, 'server.log')
|
|
41
|
-
const DEFAULT_PORT = 8788
|
|
42
|
-
|
|
43
40
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
44
41
|
|
|
45
|
-
function readPid() {
|
|
42
|
+
function readPid(file) {
|
|
46
43
|
try {
|
|
47
|
-
const pid = parseInt(readFileSync(
|
|
44
|
+
const pid = parseInt(readFileSync(file, 'utf-8').trim())
|
|
48
45
|
return Number.isFinite(pid) ? pid : null
|
|
49
46
|
} catch {
|
|
50
47
|
return null
|
|
@@ -60,37 +57,36 @@ function isRunning(pid) {
|
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
function writePid(pid) {
|
|
60
|
+
function writePid(file, pid) {
|
|
64
61
|
mkdirSync(WEB_UI_HOME, { recursive: true })
|
|
65
|
-
writeFileSync(
|
|
62
|
+
writeFileSync(file, String(pid))
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
function removePid() {
|
|
69
|
-
try { unlinkSync(
|
|
65
|
+
function removePid(file) {
|
|
66
|
+
try { unlinkSync(file) } catch {}
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
function getPid() {
|
|
73
|
-
const pid = readPid()
|
|
69
|
+
function getPid(file) {
|
|
70
|
+
const pid = readPid(file)
|
|
74
71
|
if (pid && isRunning(pid)) return pid
|
|
75
|
-
removePid()
|
|
72
|
+
removePid(file)
|
|
76
73
|
return null
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
function
|
|
80
|
-
|
|
76
|
+
function getApiPid() { return getPid(API_PID_FILE) }
|
|
77
|
+
function getWebPid() { return getPid(WEB_PID_FILE) }
|
|
78
|
+
|
|
79
|
+
function getPort(argName, defaultPort) {
|
|
80
|
+
const idx = process.argv.indexOf(argName)
|
|
81
81
|
if (idx !== -1 && process.argv[idx + 1]) {
|
|
82
82
|
const p = parseInt(process.argv[idx + 1])
|
|
83
83
|
if (!isNaN(p)) return p
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
return parseInt(process.env.HERMIUM_PORT)
|
|
87
|
-
}
|
|
88
|
-
return DEFAULT_PORT
|
|
85
|
+
return defaultPort
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
function getRunningPort(pid) {
|
|
92
89
|
if (!pid) return null
|
|
93
|
-
|
|
94
90
|
try {
|
|
95
91
|
if (process.platform === 'win32') {
|
|
96
92
|
const out = execSync(`netstat -aon -p tcp | findstr LISTENING | findstr " ${pid}$"`, { encoding: 'utf-8' }).trim()
|
|
@@ -99,7 +95,6 @@ function getRunningPort(pid) {
|
|
|
99
95
|
const port = address?.split(':').pop()
|
|
100
96
|
return port ? parseInt(port, 10) : null
|
|
101
97
|
}
|
|
102
|
-
|
|
103
98
|
const out = execSync(`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`, { encoding: 'utf-8' }).trim()
|
|
104
99
|
const lines = out.split('\n').slice(1)
|
|
105
100
|
for (const line of lines) {
|
|
@@ -110,150 +105,190 @@ function getRunningPort(pid) {
|
|
|
110
105
|
return null
|
|
111
106
|
}
|
|
112
107
|
|
|
108
|
+
function rotateLog(file) {
|
|
109
|
+
try {
|
|
110
|
+
const st = statSync(file)
|
|
111
|
+
if (st.size > 3 * 1024 * 1024) {
|
|
112
|
+
const content = readFileSync(file, 'utf-8')
|
|
113
|
+
const kept = content.split('\n').slice(-2000)
|
|
114
|
+
writeFileSync(file, kept.join('\n'))
|
|
115
|
+
console.log(pc.cyan(` ↻ Rotated ${file.replace(homedir(), '~')}`))
|
|
116
|
+
}
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function spawnServer({ name, entry, logFile, pidFile, port, env, runtime }) {
|
|
121
|
+
if (!existsSync(entry)) {
|
|
122
|
+
console.log(pc.red(` ✗ ${name} not found: ${entry}`))
|
|
123
|
+
console.log(` Run "hermium build" first (or check your installation)`)
|
|
124
|
+
process.exit(1)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
rotateLog(logFile)
|
|
128
|
+
|
|
129
|
+
const logFd = openSync(logFile, 'a')
|
|
130
|
+
const child = spawn(runtime, [entry], {
|
|
131
|
+
detached: true,
|
|
132
|
+
stdio: ['ignore', logFd, logFd],
|
|
133
|
+
env: { ...process.env, ...env, NODE_ENV: 'production' },
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
child.on('error', (err) => {
|
|
137
|
+
console.error(pc.red(` ✗ Failed to start ${name}: ${err.message}`))
|
|
138
|
+
removePid(pidFile)
|
|
139
|
+
process.exit(1)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
child.unref()
|
|
143
|
+
writePid(pidFile, child.pid)
|
|
144
|
+
return child.pid
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stopPid(pid, name) {
|
|
148
|
+
if (!pid) return false
|
|
149
|
+
try {
|
|
150
|
+
process.kill(pid, 'SIGTERM')
|
|
151
|
+
const start = Date.now()
|
|
152
|
+
while (isRunning(pid) && Date.now() - start < 5000) {
|
|
153
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
|
|
154
|
+
}
|
|
155
|
+
if (isRunning(pid)) process.kill(pid, 'SIGKILL')
|
|
156
|
+
return true
|
|
157
|
+
} catch {
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
113
162
|
// ─── Commands ──────────────────────────────────────────────────────────────
|
|
114
163
|
|
|
115
164
|
function cmdStatus() {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
165
|
+
const apiPid = getApiPid()
|
|
166
|
+
const webPid = getWebPid()
|
|
167
|
+
const apiPort = apiPid ? getRunningPort(apiPid) : null
|
|
168
|
+
const webPort = webPid ? getRunningPort(webPid) : null
|
|
169
|
+
|
|
170
|
+
if (apiPid || webPid) {
|
|
119
171
|
console.log(pc.green(` ✓ Hermium is running`))
|
|
120
|
-
console.log(`
|
|
121
|
-
console.log(`
|
|
122
|
-
console.log(`
|
|
172
|
+
if (apiPid) console.log(` API : PID ${apiPid}, port ${apiPort || 'unknown'}`)
|
|
173
|
+
if (webPid) console.log(` Web : PID ${webPid}, port ${webPort || 'unknown'}`)
|
|
174
|
+
console.log(` Logs : ${WEB_UI_HOME}`)
|
|
123
175
|
} else {
|
|
124
176
|
console.log(pc.yellow(` ⊘ Hermium is not running`))
|
|
125
177
|
}
|
|
126
178
|
}
|
|
127
179
|
|
|
128
|
-
function cmdStart() {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.log(`
|
|
180
|
+
async function cmdStart() {
|
|
181
|
+
const existingApi = getApiPid()
|
|
182
|
+
const existingWeb = getWebPid()
|
|
183
|
+
if (existingApi || existingWeb) {
|
|
184
|
+
console.log(pc.yellow(` ✗ Hermium is already running`))
|
|
185
|
+
if (existingApi) console.log(` API PID: ${existingApi}`)
|
|
186
|
+
if (existingWeb) console.log(` Web PID: ${existingWeb}`)
|
|
187
|
+
console.log(` Use "hermium stop" first`)
|
|
133
188
|
process.exit(1)
|
|
134
189
|
}
|
|
135
190
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
process.exit(1)
|
|
140
|
-
}
|
|
191
|
+
const apiPort = getPort('--port', DEFAULT_API_PORT)
|
|
192
|
+
const webPort = getPort('--web-port', DEFAULT_WEB_PORT)
|
|
193
|
+
const runtime = getRuntimeCmd()
|
|
141
194
|
|
|
142
|
-
const port = getPort()
|
|
143
195
|
mkdirSync(WEB_UI_HOME, { recursive: true })
|
|
144
196
|
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
const st = statSync(LOG_FILE)
|
|
148
|
-
if (st.size > 3 * 1024 * 1024) {
|
|
149
|
-
const content = readFileSync(LOG_FILE, 'utf-8')
|
|
150
|
-
const kept = content.split('\n').slice(-2000)
|
|
151
|
-
writeFileSync(LOG_FILE, kept.join('\n'))
|
|
152
|
-
console.log(pc.cyan(` ↻ Log rotated`))
|
|
153
|
-
}
|
|
154
|
-
} catch {}
|
|
197
|
+
console.log(pc.cyan(` ⏳ Starting Hermium (API:${apiPort}, Web:${webPort})...`))
|
|
155
198
|
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
199
|
+
// 1. Start API server
|
|
200
|
+
const apiPid = spawnServer({
|
|
201
|
+
name: 'API server',
|
|
202
|
+
entry: apiEntry,
|
|
203
|
+
logFile: API_LOG_FILE,
|
|
204
|
+
pidFile: API_PID_FILE,
|
|
205
|
+
port: apiPort,
|
|
206
|
+
runtime,
|
|
161
207
|
env: {
|
|
162
|
-
|
|
163
|
-
|
|
208
|
+
HERMIUM_PORT: String(apiPort),
|
|
209
|
+
HERMIUM_WEB_PORT: String(webPort),
|
|
164
210
|
WEB_STATIC_DIR: resolve(pkgDir, 'dist', 'web'),
|
|
165
|
-
HERMIUM_PORT: String(port),
|
|
166
211
|
},
|
|
167
212
|
})
|
|
168
213
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
214
|
+
// 2. Start web SSR server
|
|
215
|
+
const webPid = spawnServer({
|
|
216
|
+
name: 'Web server',
|
|
217
|
+
entry: webEntry,
|
|
218
|
+
logFile: WEB_LOG_FILE,
|
|
219
|
+
pidFile: WEB_PID_FILE,
|
|
220
|
+
port: webPort,
|
|
221
|
+
runtime,
|
|
222
|
+
env: { PORT: String(webPort) },
|
|
173
223
|
})
|
|
174
224
|
|
|
175
|
-
|
|
176
|
-
|
|
225
|
+
console.log(` API PID: ${apiPid}`)
|
|
226
|
+
console.log(` Web PID: ${webPid}`)
|
|
177
227
|
|
|
178
|
-
// Poll health
|
|
179
|
-
const healthUrl = `http://127.0.0.1:${port}/api/health`
|
|
228
|
+
// Poll web server health
|
|
180
229
|
const maxWait = 30000
|
|
181
230
|
const interval = 500
|
|
182
231
|
let waited = 0
|
|
183
|
-
|
|
184
|
-
console.log(pc.cyan(` ⏳ Starting Hermium (PID: ${child.pid}, port: ${port})...`))
|
|
232
|
+
const webUrl = `http://localhost:${webPort}`
|
|
185
233
|
|
|
186
234
|
function poll() {
|
|
187
235
|
waited += interval
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
console.log(`
|
|
191
|
-
|
|
236
|
+
|
|
237
|
+
if (!isRunning(apiPid)) {
|
|
238
|
+
console.log(pc.red(` ✗ API server crashed`))
|
|
239
|
+
console.log(` Check log: ${API_LOG_FILE}`)
|
|
240
|
+
stopPid(webPid, 'web')
|
|
241
|
+
removePid(API_PID_FILE)
|
|
242
|
+
removePid(WEB_PID_FILE)
|
|
243
|
+
process.exit(1)
|
|
244
|
+
}
|
|
245
|
+
if (!isRunning(webPid)) {
|
|
246
|
+
console.log(pc.red(` ✗ Web server crashed`))
|
|
247
|
+
console.log(` Check log: ${WEB_LOG_FILE}`)
|
|
248
|
+
stopPid(apiPid, 'api')
|
|
249
|
+
removePid(API_PID_FILE)
|
|
250
|
+
removePid(WEB_PID_FILE)
|
|
192
251
|
process.exit(1)
|
|
193
252
|
}
|
|
194
253
|
|
|
195
|
-
fetch(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
execSync(openCmd, { stdio: 'ignore' })
|
|
211
|
-
} catch {}
|
|
212
|
-
} else if (waited < maxWait) {
|
|
213
|
-
setTimeout(poll, interval)
|
|
214
|
-
} else {
|
|
215
|
-
console.log(pc.red(` ✗ Timed out waiting for server`))
|
|
216
|
-
console.log(` Check log: ${LOG_FILE}`)
|
|
217
|
-
removePid()
|
|
218
|
-
process.exit(1)
|
|
219
|
-
}
|
|
220
|
-
})
|
|
221
|
-
.catch(() => {
|
|
222
|
-
if (waited < maxWait) {
|
|
223
|
-
setTimeout(poll, interval)
|
|
224
|
-
} else {
|
|
225
|
-
console.log(pc.red(` ✗ Timed out waiting for server`))
|
|
226
|
-
console.log(` Check log: ${LOG_FILE}`)
|
|
227
|
-
removePid()
|
|
228
|
-
process.exit(1)
|
|
229
|
-
}
|
|
230
|
-
})
|
|
254
|
+
fetch(`${webUrl}/api/health`).catch(() => null).then((res) => {
|
|
255
|
+
// Web server doesn't have /api/health, this will 404 but we just want to check it's listening
|
|
256
|
+
if (waited < maxWait) {
|
|
257
|
+
setTimeout(poll, interval)
|
|
258
|
+
} else {
|
|
259
|
+
console.log(pc.green(` ✓ Hermium started`))
|
|
260
|
+
console.log(` ${webUrl}`)
|
|
261
|
+
console.log(` Logs: ${WEB_UI_HOME}`)
|
|
262
|
+
const openCmd =
|
|
263
|
+
process.platform === 'win32' ? `start ${webUrl}` :
|
|
264
|
+
process.platform === 'darwin' ? `open ${webUrl}` :
|
|
265
|
+
`xdg-open ${webUrl}`
|
|
266
|
+
try { execSync(openCmd, { stdio: 'ignore' }) } catch {}
|
|
267
|
+
}
|
|
268
|
+
})
|
|
231
269
|
}
|
|
232
270
|
|
|
233
271
|
setTimeout(poll, interval)
|
|
234
272
|
}
|
|
235
273
|
|
|
236
274
|
function cmdStop() {
|
|
237
|
-
const
|
|
238
|
-
|
|
275
|
+
const apiPid = getApiPid()
|
|
276
|
+
const webPid = getWebPid()
|
|
277
|
+
|
|
278
|
+
if (!apiPid && !webPid) {
|
|
239
279
|
console.log(pc.yellow(` ⊘ Hermium is not running`))
|
|
240
280
|
return
|
|
241
281
|
}
|
|
242
282
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
removePid()
|
|
253
|
-
console.log(pc.green(` ✓ Hermium stopped (PID: ${pid})`))
|
|
254
|
-
} catch {
|
|
255
|
-
removePid()
|
|
256
|
-
console.log(pc.green(` ✓ Hermium stopped (PID: ${pid})`))
|
|
283
|
+
if (webPid) {
|
|
284
|
+
stopPid(webPid, 'web')
|
|
285
|
+
removePid(WEB_PID_FILE)
|
|
286
|
+
console.log(pc.green(` ✓ Web server stopped (PID: ${webPid})`))
|
|
287
|
+
}
|
|
288
|
+
if (apiPid) {
|
|
289
|
+
stopPid(apiPid, 'api')
|
|
290
|
+
removePid(API_PID_FILE)
|
|
291
|
+
console.log(pc.green(` ✓ API server stopped (PID: ${apiPid})`))
|
|
257
292
|
}
|
|
258
293
|
}
|
|
259
294
|
|
|
@@ -271,18 +306,19 @@ ${pc.bold('Usage:')}
|
|
|
271
306
|
hermium <command> [options]
|
|
272
307
|
|
|
273
308
|
${pc.bold('Commands:')}
|
|
274
|
-
start Start Hermium
|
|
275
|
-
stop Stop Hermium
|
|
276
|
-
restart Restart Hermium
|
|
309
|
+
start Start Hermium servers (daemon)
|
|
310
|
+
stop Stop Hermium servers
|
|
311
|
+
restart Restart Hermium servers
|
|
277
312
|
status Show running status
|
|
278
313
|
help Show this help message
|
|
279
314
|
|
|
280
315
|
${pc.bold('Options:')}
|
|
281
|
-
--port <n>
|
|
316
|
+
--port <n> API port (default: 8788)
|
|
317
|
+
--web-port <n> Web port (default: 3000)
|
|
282
318
|
|
|
283
319
|
${pc.bold('Examples:')}
|
|
284
320
|
hermium start
|
|
285
|
-
hermium start --port
|
|
321
|
+
hermium start --port 9000 --web-port 3001
|
|
286
322
|
hermium stop
|
|
287
323
|
hermium status
|
|
288
324
|
`)
|