@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.
Files changed (2) hide show
  1. package/bin/holin-cli.js +60 -38
  2. 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
- if (!credsMissingSince) {
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
- if (isProxyRunning()) {
606
- // Proxy process exists per pid file also verify port is actually listening
607
- waitForPort(PROXY_PORT, PROXY_HOST, 3000).then(resolve).catch(resolve); // best-effort
608
- return;
609
- }
610
-
611
- ensureConfigDir();
612
- const logFd = openSync(LOG_FILE, "a");
613
-
614
- const child = spawn(process.execPath, [process.argv[1], "proxy", "start", "--daemon"], {
615
- detached: true,
616
- stdio: ["ignore", logFd, logFd],
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
- child.unref();
641
+ child.unref();
620
642
 
621
- // Wait until the proxy port is actually accepting connections (up to 10s).
622
- // This replaces the old pid-file poll which only confirmed the process existed,
623
- // not that it was ready to serve — causing ECONNREFUSED races on slow machines.
624
- waitForPort(PROXY_PORT, PROXY_HOST, 10000).then(resolve).catch(reject);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holin-work/holin-cli",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "Holin CLI — Accio plugin authorization tool for ICBU batch listing",
5
5
  "type": "module",
6
6
  "bin": {