contactsheet 0.1.3 → 0.1.5

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.
@@ -1187,3 +1187,34 @@ body:not([data-sidebar="off"]) #cs-hud { left: calc(var(--cs-sb-w) + 16px); }
1187
1187
  box-shadow: 0 0 0 3px var(--cs-accent), 0 0 0 6px var(--cs-accent-32);
1188
1188
  z-index: 4;
1189
1189
  }
1190
+
1191
+ /* ---------- dev server 僵死横幅(全局,盖在一切之上但不拦事件之外的区域) ---------- */
1192
+ .cs-health {
1193
+ position: fixed;
1194
+ top: 12px;
1195
+ left: 50%;
1196
+ transform: translateX(-50%);
1197
+ z-index: var(--cs-z-overlay);
1198
+ display: flex;
1199
+ align-items: baseline;
1200
+ gap: var(--cs-sp-4);
1201
+ max-width: min(720px, 86vw);
1202
+ padding: var(--cs-sp-4) var(--cs-sp-6);
1203
+ border: 1px solid var(--cs-danger);
1204
+ border-radius: var(--cs-r-3);
1205
+ background: color-mix(in srgb, var(--cs-danger) 12%, var(--cs-s1));
1206
+ color: var(--cs-fg);
1207
+ font: var(--cs-fs-sm)/1.5 var(--cs-ff-sans);
1208
+ box-shadow: var(--cs-e3);
1209
+ /* 入场:@starting-style 由 transition 接住,无 JS 时序(MOTION-013);进入用 ease-out(MOTION-006) */
1210
+ transition: opacity var(--cs-dur-3) var(--cs-ease-out), translate var(--cs-dur-3) var(--cs-ease-out);
1211
+ }
1212
+ @starting-style {
1213
+ .cs-health { opacity: 0; translate: 0 -8px; }
1214
+ }
1215
+ .cs-health b { color: var(--cs-danger); font-weight: var(--cs-fw-bold); white-space: nowrap; }
1216
+ .cs-health span { color: var(--cs-fg-2); }
1217
+ @media (prefers-reduced-motion: reduce) {
1218
+ .cs-health { transition: opacity var(--cs-dur-3) var(--cs-ease-out); }
1219
+ @starting-style { .cs-health { translate: none; } }
1220
+ }
package/dist/cli.js CHANGED
@@ -1147,6 +1147,56 @@ async function pushToSession(cfg, text2, pid) {
1147
1147
  return { ok: true, pid: target.pid, name: target.name };
1148
1148
  }
1149
1149
 
1150
+ // src/server/health.ts
1151
+ var INTERVAL_MS = Number(process.env.CS_HEALTH_INTERVAL_MS ?? 2e4);
1152
+ var TIMEOUT_MS = Number(process.env.CS_HEALTH_TIMEOUT_MS ?? 1e4);
1153
+ var FAILS_TO_TRIP = 2;
1154
+ var current = { ok: true };
1155
+ function currentHealth() {
1156
+ return current;
1157
+ }
1158
+ function startHealthMonitor(cfg, broadcast2) {
1159
+ let fails = 0;
1160
+ let everOk = false;
1161
+ let timer = null;
1162
+ const probe = async () => {
1163
+ let failDetail = null;
1164
+ try {
1165
+ const r = await fetch(`${cfg.target}/__cs/registry`, { signal: AbortSignal.timeout(TIMEOUT_MS) });
1166
+ if (r.ok || r.status === 404) {
1167
+ everOk = true;
1168
+ fails = 0;
1169
+ if (!current.ok) {
1170
+ current = { ok: true, lastOkAt: Date.now() };
1171
+ broadcast2({ type: "health", ok: true });
1172
+ } else {
1173
+ current = { ok: true, lastOkAt: Date.now() };
1174
+ }
1175
+ return;
1176
+ }
1177
+ failDetail = `SSR \u63A2\u6D4B\u8FD4\u56DE HTTP ${r.status}`;
1178
+ } catch (err) {
1179
+ const e = err;
1180
+ failDetail = e.name === "TimeoutError" ? `SSR \u63A2\u6D4B ${TIMEOUT_MS / 1e3}s \u65E0\u54CD\u5E94` : `\u8FDE\u4E0D\u4E0A(${e.cause?.code ?? "network"})`;
1181
+ }
1182
+ if (!everOk) return;
1183
+ fails++;
1184
+ if (fails >= FAILS_TO_TRIP && current.ok) {
1185
+ current = { ok: false, detail: failDetail ?? "\u672A\u77E5", lastOkAt: current.lastOkAt };
1186
+ broadcast2({ type: "health", ok: false, detail: current.detail });
1187
+ console.warn(`[contactsheet] \u26A0 \u76EE\u6807 ${cfg.target} \u7591\u4F3C\u50F5\u6B7B:${current.detail}(\u9759\u6001\u53EF\u80FD\u4ECD 200)\u3002\u753B\u5E03\u5DF2\u6302\u6A2A\u5E45\u3002`);
1188
+ }
1189
+ };
1190
+ timer = setInterval(() => void probe(), INTERVAL_MS);
1191
+ timer.unref();
1192
+ void probe();
1193
+ return {
1194
+ close() {
1195
+ if (timer) clearInterval(timer);
1196
+ }
1197
+ };
1198
+ }
1199
+
1150
1200
  // src/server/sse.ts
1151
1201
  var clients = /* @__PURE__ */ new Set();
1152
1202
  var heartbeat = null;
@@ -1383,6 +1433,10 @@ async function handleApi(cfg, req, res, pathname) {
1383
1433
  const rel = await saveRef(cfg, body.name || "ref.png", body.dataBase64);
1384
1434
  return sendJson(res, 200, { path: rel });
1385
1435
  }
1436
+ if (pathname === "/__cs/api/health") {
1437
+ if (method !== "GET") return sendText(res, 405, "method not allowed");
1438
+ return sendJson(res, 200, currentHealth());
1439
+ }
1386
1440
  if (pathname === "/__cs/api/context") {
1387
1441
  if (method !== "GET") return sendText(res, 405, "method not allowed");
1388
1442
  try {
@@ -1559,6 +1613,11 @@ async function startServer(cfg) {
1559
1613
  res.destroy();
1560
1614
  }
1561
1615
  });
1616
+ proxy.on("proxyReq", (proxyReq, _req, res) => {
1617
+ res.on("close", () => {
1618
+ if (!res.writableEnded) proxyReq.destroy();
1619
+ });
1620
+ });
1562
1621
  proxy.on("proxyRes", () => proxyLog.alive());
1563
1622
  proxy.on("open", () => proxyLog.alive());
1564
1623
  let mcp = null;
@@ -1581,18 +1640,30 @@ async function startServer(cfg) {
1581
1640
  s.on("close", () => sockets.delete(s));
1582
1641
  });
1583
1642
  const host = cfg.host ?? "127.0.0.1";
1584
- const port = await listenWithFallback(server, cfg, host);
1643
+ let twin = null;
1644
+ const port = await listenWithFallback(server, cfg, host, (t) => {
1645
+ twin = t;
1646
+ t.on("connection", (sock) => {
1647
+ sockets.add(sock);
1648
+ sock.on("close", () => sockets.delete(sock));
1649
+ });
1650
+ });
1585
1651
  if (host !== "127.0.0.1" && host !== "localhost") {
1586
1652
  console.warn(
1587
1653
  warn(`\u76D1\u542C\u5728 ${host}:${port} \u2014\u2014 \u753B\u5E03\u65E0\u9274\u6743,\u540C\u7F51\u7EDC\u7684\u4EFB\u4F55\u4EBA\u90FD\u80FD\u8BFB\u4F60\u7684\u6279\u6CE8\u3001\u5E76\u5411\u4F60\u7684 Claude Code \u4F1A\u8BDD\u63A8\u9001\u6D88\u606F\u3002\u53EA\u5728\u53EF\u4FE1\u7F51\u7EDC\u8FD9\u4E48\u505A\u3002`)
1588
1654
  );
1589
1655
  }
1656
+ const health = startHealthMonitor(cfg, broadcast);
1590
1657
  return {
1591
1658
  port,
1592
1659
  async close() {
1660
+ health.close();
1593
1661
  closeAll();
1594
1662
  proxyLog.close();
1595
1663
  proxy.close();
1664
+ if (twin) {
1665
+ await new Promise((r) => twin.close(() => r()));
1666
+ }
1596
1667
  await new Promise((resolve2) => {
1597
1668
  const fallback = setTimeout(resolve2, 2e3);
1598
1669
  for (const s of sockets) s.destroy();
@@ -1674,18 +1745,34 @@ async function occupiedBySameProject(cfg, port) {
1674
1745
  return false;
1675
1746
  }
1676
1747
  }
1677
- async function listenWithFallback(server, cfg, host) {
1678
- const tryListen = (port) => new Promise((resolve2, reject) => {
1748
+ async function listenWithFallback(server, cfg, host, onTwin) {
1749
+ const listenOnce = (srv, port, h) => new Promise((resolve2, reject) => {
1679
1750
  const onErr = (err) => {
1680
- if (err.code === "EADDRINUSE") resolve2(false);
1751
+ if (err.code === "EADDRINUSE") resolve2("busy");
1752
+ else if (err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT") resolve2("nofam");
1681
1753
  else reject(err);
1682
1754
  };
1683
- server.once("error", onErr);
1684
- server.listen(port, host, () => {
1685
- server.off("error", onErr);
1686
- resolve2(true);
1755
+ srv.once("error", onErr);
1756
+ srv.listen(port, h, () => {
1757
+ srv.off("error", onErr);
1758
+ resolve2("ok");
1687
1759
  });
1688
1760
  });
1761
+ const tryListen = async (port) => {
1762
+ const main2 = await listenOnce(server, port, host);
1763
+ if (main2 !== "ok") return false;
1764
+ if (host !== "127.0.0.1") return true;
1765
+ const t = http.createServer(server.listeners("request")[0]);
1766
+ for (const l of server.listeners("upgrade")) t.on("upgrade", l);
1767
+ const r = await listenOnce(t, port, "::1");
1768
+ if (r === "ok") {
1769
+ onTwin(t);
1770
+ return true;
1771
+ }
1772
+ if (r === "nofam") return true;
1773
+ await new Promise((res) => server.close(() => res()));
1774
+ return false;
1775
+ };
1689
1776
  if (await tryListen(cfg.port)) return cfg.port;
1690
1777
  if (await occupiedBySameProject(cfg, cfg.port)) {
1691
1778
  console.log(