@holin-work/holin-cli 1.3.6 → 1.3.8
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/holin-cli.js +60 -38
- package/package.json +1 -1
package/bin/holin-cli.js
CHANGED
|
@@ -34,6 +34,7 @@ const CONFIG_DIR = join(
|
|
|
34
34
|
const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
35
35
|
const PID_FILE = join(CONFIG_DIR, "proxy.pid");
|
|
36
36
|
const LOG_FILE = join(CONFIG_DIR, "proxy.log");
|
|
37
|
+
const LOGIN_LOCK_FILE = join(CONFIG_DIR, "login.lock"); // written by auth login, read by proxy healthcheck
|
|
37
38
|
|
|
38
39
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
39
40
|
|
|
@@ -234,6 +235,25 @@ function createProxyServer() {
|
|
|
234
235
|
return;
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
// Health check endpoint — must be before credential check so waitForPort probes always succeed
|
|
239
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
240
|
+
const creds = readCredentials();
|
|
241
|
+
const body = JSON.stringify({
|
|
242
|
+
status: "ok",
|
|
243
|
+
authenticated: !!(creds?.api_key),
|
|
244
|
+
customer: creds?.customer_name || null,
|
|
245
|
+
plan: creds?.plan || null,
|
|
246
|
+
upstream: UPSTREAM_URL,
|
|
247
|
+
pid: process.pid,
|
|
248
|
+
});
|
|
249
|
+
res.writeHead(200, {
|
|
250
|
+
"Content-Type": "application/json",
|
|
251
|
+
"Content-Length": Buffer.byteLength(body),
|
|
252
|
+
});
|
|
253
|
+
res.end(body);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
237
257
|
// Read credentials on every request (hot-reload support)
|
|
238
258
|
const creds = readCredentials();
|
|
239
259
|
if (!creds?.api_key) {
|
|
@@ -248,24 +268,6 @@ function createProxyServer() {
|
|
|
248
268
|
return;
|
|
249
269
|
}
|
|
250
270
|
|
|
251
|
-
// Health check endpoint
|
|
252
|
-
if (req.method === "GET" && req.url === "/health") {
|
|
253
|
-
const body = JSON.stringify({
|
|
254
|
-
status: "ok",
|
|
255
|
-
authenticated: !!(creds && creds.api_key),
|
|
256
|
-
customer: creds?.customer_name || null,
|
|
257
|
-
plan: creds?.plan || null,
|
|
258
|
-
upstream: UPSTREAM_URL,
|
|
259
|
-
pid: process.pid,
|
|
260
|
-
});
|
|
261
|
-
res.writeHead(200, {
|
|
262
|
-
"Content-Type": "application/json",
|
|
263
|
-
"Content-Length": Buffer.byteLength(body),
|
|
264
|
-
});
|
|
265
|
-
res.end(body);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
271
|
// Build upstream request
|
|
270
272
|
// MCP streamable-http has a single endpoint; forward directly to upstream path.
|
|
271
273
|
// Accio requests http://127.0.0.1:18787/mcp → upstream https://api.holin.work/icbu/mcp
|
|
@@ -388,6 +390,14 @@ async function cmdAuthLogin() {
|
|
|
388
390
|
process.exit(1);
|
|
389
391
|
}
|
|
390
392
|
|
|
393
|
+
// Write login lock so the running proxy does not exit during re-auth credential gap.
|
|
394
|
+
ensureConfigDir();
|
|
395
|
+
writeFileSync(LOGIN_LOCK_FILE, String(process.pid), "utf-8");
|
|
396
|
+
const removeLock = () => { try { if (existsSync(LOGIN_LOCK_FILE)) unlinkSync(LOGIN_LOCK_FILE); } catch {} };
|
|
397
|
+
process.on("exit", removeLock);
|
|
398
|
+
process.on("SIGINT", () => { removeLock(); process.exit(1); });
|
|
399
|
+
process.on("SIGTERM", () => { removeLock(); process.exit(1); });
|
|
400
|
+
|
|
391
401
|
console.log("[holin-cli] Verifying API Key...");
|
|
392
402
|
let result;
|
|
393
403
|
try {
|
|
@@ -430,6 +440,7 @@ async function cmdAuthLogin() {
|
|
|
430
440
|
console.error(`[holin-cli] Warning: Failed to start local proxy — ${err.message}`);
|
|
431
441
|
console.error("[holin-cli] You can start it manually with: holin-cli proxy start");
|
|
432
442
|
}
|
|
443
|
+
// Lock removed automatically via process.on("exit")
|
|
433
444
|
}
|
|
434
445
|
|
|
435
446
|
function cmdAuthLogout() {
|
|
@@ -528,12 +539,20 @@ function cmdProxyStart() {
|
|
|
528
539
|
return;
|
|
529
540
|
}
|
|
530
541
|
// Check if credentials file still exists (user may have logged out / uninstalled)
|
|
531
|
-
// Use a 30s grace period to avoid false positives during re-auth (old creds cleared, new ones not yet written)
|
|
542
|
+
// Use a 30s grace period to avoid false positives during re-auth (old creds cleared, new ones not yet written).
|
|
543
|
+
// Skip the grace period countdown entirely while a login.lock file exists — auth login is in progress.
|
|
532
544
|
const credsExist = existsSync(CREDENTIALS_FILE);
|
|
533
545
|
const creds = credsExist ? readCredentials() : null;
|
|
534
546
|
const hasValidKey = !!(creds?.api_key);
|
|
535
547
|
if (!hasValidKey) {
|
|
536
|
-
|
|
548
|
+
const loginInProgress = existsSync(LOGIN_LOCK_FILE);
|
|
549
|
+
if (loginInProgress) {
|
|
550
|
+
// auth login is actively running — reset timer, do not start grace period
|
|
551
|
+
if (credsMissingSince) {
|
|
552
|
+
log("[proxy] Login lock detected, resetting credentials grace period.");
|
|
553
|
+
credsMissingSince = null;
|
|
554
|
+
}
|
|
555
|
+
} else if (!credsMissingSince) {
|
|
537
556
|
credsMissingSince = Date.now();
|
|
538
557
|
log("[proxy] Credentials missing, starting 30s grace period...");
|
|
539
558
|
} else if (Date.now() - credsMissingSince > 30000) {
|
|
@@ -602,26 +621,29 @@ function waitForPort(port, host, timeoutMs) {
|
|
|
602
621
|
|
|
603
622
|
function startProxyDaemon() {
|
|
604
623
|
return new Promise((resolve, reject) => {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
624
|
+
// Always verify the port is actually reachable first, regardless of pid file.
|
|
625
|
+
// The old proxy may have exited (idle timeout) while the pid file is stale.
|
|
626
|
+
waitForPort(PROXY_PORT, PROXY_HOST, 500)
|
|
627
|
+
.then(() => {
|
|
628
|
+
// Port is already up — existing proxy is healthy, no need to spawn.
|
|
629
|
+
resolve();
|
|
630
|
+
})
|
|
631
|
+
.catch(() => {
|
|
632
|
+
// Port not reachable — spawn a new proxy daemon regardless of pid file state.
|
|
633
|
+
ensureConfigDir();
|
|
634
|
+
const logFd = openSync(LOG_FILE, "a");
|
|
635
|
+
|
|
636
|
+
const child = spawn(process.execPath, [process.argv[1], "proxy", "start", "--daemon"], {
|
|
637
|
+
detached: true,
|
|
638
|
+
stdio: ["ignore", logFd, logFd],
|
|
639
|
+
});
|
|
618
640
|
|
|
619
|
-
|
|
641
|
+
child.unref();
|
|
620
642
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
643
|
+
// Wait until the proxy port is actually accepting connections (up to 10s).
|
|
644
|
+
// Only resolve after a real HTTP response from /health — never resolve on catch.
|
|
645
|
+
waitForPort(PROXY_PORT, PROXY_HOST, 10000).then(resolve).catch(reject);
|
|
646
|
+
});
|
|
625
647
|
});
|
|
626
648
|
}
|
|
627
649
|
|