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 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 detectRuntime() {
14
- // Check if bun is available on PATH
23
+ function getRuntimeCmd() {
15
24
  try {
16
25
  execSync('bun --version', { stdio: 'ignore' })
17
- return { cmd: 'bun', found: true }
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
- return { cmd: exe, found: !!isBun }
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(PID_FILE, 'utf-8').trim())
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(PID_FILE, String(pid))
62
+ writeFileSync(file, String(pid))
66
63
  }
67
64
 
68
- function removePid() {
69
- try { unlinkSync(PID_FILE) } catch {}
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 getPort() {
80
- const idx = process.argv.indexOf('--port')
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
- if (process.env.HERMIUM_PORT && !isNaN(process.env.HERMIUM_PORT)) {
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 pid = getPid()
117
- if (pid) {
118
- const port = getRunningPort(pid)
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(` PID : ${pid}`)
121
- console.log(` Port : ${port || 'unknown'}`)
122
- console.log(` Log : ${LOG_FILE}`)
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 existing = getPid()
130
- if (existing) {
131
- console.log(pc.yellow(` Hermium is already running (PID: ${existing})`))
132
- console.log(` Use "hermium stop" to stop it first`)
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
- if (!existsSync(serverEntry)) {
137
- console.log(pc.red(` Server not found: ${serverEntry}`))
138
- console.log(` Run "hermium build" first (or check your installation)`)
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
- // Rotate log if over 3 MB
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
- const runtime = getRuntimeCmd()
157
- const logFd = openSync(LOG_FILE, 'a')
158
- const child = spawn(runtime, [serverEntry], {
159
- detached: true,
160
- stdio: ['ignore', logFd, logFd],
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
- ...process.env,
163
- NODE_ENV: 'production',
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
- child.on('error', (err) => {
170
- console.error(pc.red(` Failed to start: ${err.message}`))
171
- removePid()
172
- process.exit(1)
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
- child.unref()
176
- writePid(child.pid)
225
+ console.log(` API PID: ${apiPid}`)
226
+ console.log(` Web PID: ${webPid}`)
177
227
 
178
- // Poll health endpoint
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
- if (!isRunning(child.pid)) {
189
- console.log(pc.red(` ✗ Failed to start`))
190
- console.log(` Check log: ${LOG_FILE}`)
191
- removePid()
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(healthUrl)
196
- .then((res) => {
197
- if (res.ok) {
198
- const url = `http://localhost:${port}`
199
- console.log(pc.green(` ✓ Hermium started`))
200
- console.log(` ${url}`)
201
- console.log(` Log: ${LOG_FILE}`)
202
- // Auto-open browser
203
- const openCmd =
204
- process.platform === 'win32'
205
- ? `start ${url}`
206
- : process.platform === 'darwin'
207
- ? `open ${url}`
208
- : `xdg-open ${url}`
209
- try {
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 pid = getPid()
238
- if (!pid) {
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
- try {
244
- process.kill(pid, 'SIGTERM')
245
- const start = Date.now()
246
- while (isRunning(pid) && Date.now() - start < 5000) {
247
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
248
- }
249
- if (isRunning(pid)) {
250
- process.kill(pid, 'SIGKILL')
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 server (daemon)
275
- stop Stop Hermium server
276
- restart Restart Hermium server
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> Port to run on (default: 8788)
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 3000
321
+ hermium start --port 9000 --web-port 3001
286
322
  hermium stop
287
323
  hermium status
288
324
  `)