contactsheet 0.1.4 → 0.1.6
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/README.md +18 -4
- package/dist/canvas/app.js +149 -106
- package/dist/canvas/app.js.map +4 -4
- package/dist/canvas/style.css +31 -0
- package/dist/cli.js +102 -24
- package/dist/cli.js.map +4 -4
- package/package.json +1 -1
package/dist/canvas/style.css
CHANGED
|
@@ -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
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';const require = createRequire(import.meta.url);
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
3
9
|
|
|
4
10
|
// src/cli.ts
|
|
5
11
|
import { parseArgs } from "node:util";
|
|
@@ -29,7 +35,6 @@ function loadConfig(cwd, flags) {
|
|
|
29
35
|
// 末尾斜杠去掉,后面一律靠 `${target}/xxx` 拼
|
|
30
36
|
target: String(pick("target") ?? DEFAULT_TARGET).replace(/\/+$/, ""),
|
|
31
37
|
port,
|
|
32
|
-
portExplicit: flags.port !== void 0,
|
|
33
38
|
appDir: pick("appDir") ?? detectAppDir(projectRoot),
|
|
34
39
|
designDir: pick("designDir") ?? DEFAULT_DESIGN_DIR
|
|
35
40
|
};
|
|
@@ -1147,6 +1152,56 @@ async function pushToSession(cfg, text2, pid) {
|
|
|
1147
1152
|
return { ok: true, pid: target.pid, name: target.name };
|
|
1148
1153
|
}
|
|
1149
1154
|
|
|
1155
|
+
// src/server/health.ts
|
|
1156
|
+
var INTERVAL_MS = Number(process.env.CS_HEALTH_INTERVAL_MS ?? 2e4);
|
|
1157
|
+
var TIMEOUT_MS = Number(process.env.CS_HEALTH_TIMEOUT_MS ?? 1e4);
|
|
1158
|
+
var FAILS_TO_TRIP = 2;
|
|
1159
|
+
var current = { ok: true };
|
|
1160
|
+
function currentHealth() {
|
|
1161
|
+
return current;
|
|
1162
|
+
}
|
|
1163
|
+
function startHealthMonitor(cfg, broadcast2) {
|
|
1164
|
+
let fails = 0;
|
|
1165
|
+
let everOk = false;
|
|
1166
|
+
let timer = null;
|
|
1167
|
+
const probe = async () => {
|
|
1168
|
+
let failDetail = null;
|
|
1169
|
+
try {
|
|
1170
|
+
const r = await fetch(`${cfg.target}/__cs/registry`, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
1171
|
+
if (r.ok || r.status === 404) {
|
|
1172
|
+
everOk = true;
|
|
1173
|
+
fails = 0;
|
|
1174
|
+
if (!current.ok) {
|
|
1175
|
+
current = { ok: true, lastOkAt: Date.now() };
|
|
1176
|
+
broadcast2({ type: "health", ok: true });
|
|
1177
|
+
} else {
|
|
1178
|
+
current = { ok: true, lastOkAt: Date.now() };
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
failDetail = `SSR \u63A2\u6D4B\u8FD4\u56DE HTTP ${r.status}`;
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
const e = err;
|
|
1185
|
+
failDetail = e.name === "TimeoutError" ? `SSR \u63A2\u6D4B ${TIMEOUT_MS / 1e3}s \u65E0\u54CD\u5E94` : `\u8FDE\u4E0D\u4E0A(${e.cause?.code ?? "network"})`;
|
|
1186
|
+
}
|
|
1187
|
+
if (!everOk) return;
|
|
1188
|
+
fails++;
|
|
1189
|
+
if (fails >= FAILS_TO_TRIP && current.ok) {
|
|
1190
|
+
current = { ok: false, detail: failDetail ?? "\u672A\u77E5", lastOkAt: current.lastOkAt };
|
|
1191
|
+
broadcast2({ type: "health", ok: false, detail: current.detail });
|
|
1192
|
+
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`);
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
timer = setInterval(() => void probe(), INTERVAL_MS);
|
|
1196
|
+
timer.unref();
|
|
1197
|
+
void probe();
|
|
1198
|
+
return {
|
|
1199
|
+
close() {
|
|
1200
|
+
if (timer) clearInterval(timer);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1150
1205
|
// src/server/sse.ts
|
|
1151
1206
|
var clients = /* @__PURE__ */ new Set();
|
|
1152
1207
|
var heartbeat = null;
|
|
@@ -1383,6 +1438,10 @@ async function handleApi(cfg, req, res, pathname) {
|
|
|
1383
1438
|
const rel = await saveRef(cfg, body.name || "ref.png", body.dataBase64);
|
|
1384
1439
|
return sendJson(res, 200, { path: rel });
|
|
1385
1440
|
}
|
|
1441
|
+
if (pathname === "/__cs/api/health") {
|
|
1442
|
+
if (method !== "GET") return sendText(res, 405, "method not allowed");
|
|
1443
|
+
return sendJson(res, 200, currentHealth());
|
|
1444
|
+
}
|
|
1386
1445
|
if (pathname === "/__cs/api/context") {
|
|
1387
1446
|
if (method !== "GET") return sendText(res, 405, "method not allowed");
|
|
1388
1447
|
try {
|
|
@@ -1559,6 +1618,11 @@ async function startServer(cfg) {
|
|
|
1559
1618
|
res.destroy();
|
|
1560
1619
|
}
|
|
1561
1620
|
});
|
|
1621
|
+
proxy.on("proxyReq", (proxyReq, _req, res) => {
|
|
1622
|
+
res.on("close", () => {
|
|
1623
|
+
if (!res.writableEnded) proxyReq.destroy();
|
|
1624
|
+
});
|
|
1625
|
+
});
|
|
1562
1626
|
proxy.on("proxyRes", () => proxyLog.alive());
|
|
1563
1627
|
proxy.on("open", () => proxyLog.alive());
|
|
1564
1628
|
let mcp = null;
|
|
@@ -1594,9 +1658,11 @@ async function startServer(cfg) {
|
|
|
1594
1658
|
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`)
|
|
1595
1659
|
);
|
|
1596
1660
|
}
|
|
1661
|
+
const health = startHealthMonitor(cfg, broadcast);
|
|
1597
1662
|
return {
|
|
1598
1663
|
port,
|
|
1599
1664
|
async close() {
|
|
1665
|
+
health.close();
|
|
1600
1666
|
closeAll();
|
|
1601
1667
|
proxyLog.close();
|
|
1602
1668
|
proxy.close();
|
|
@@ -1673,15 +1739,27 @@ function downPage(target) {
|
|
|
1673
1739
|
function escapeHtml(s) {
|
|
1674
1740
|
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
1675
1741
|
}
|
|
1676
|
-
async function
|
|
1742
|
+
async function probeSameProject(cfg, port) {
|
|
1743
|
+
for (let i = 0; i < 2; i++) {
|
|
1744
|
+
try {
|
|
1745
|
+
const r = await fetch(`http://127.0.0.1:${port}/__cs/api/state`, {
|
|
1746
|
+
signal: AbortSignal.timeout(2500)
|
|
1747
|
+
});
|
|
1748
|
+
if (!r.ok) return null;
|
|
1749
|
+
const data = await r.json();
|
|
1750
|
+
return data?.projectRoot === cfg.projectRoot ? { version: data.version } : null;
|
|
1751
|
+
} catch {
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return null;
|
|
1755
|
+
}
|
|
1756
|
+
function occupierPid(port) {
|
|
1677
1757
|
try {
|
|
1678
|
-
const
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
const data = await r.json();
|
|
1682
|
-
return data?.projectRoot === cfg.projectRoot;
|
|
1758
|
+
const { execSync } = __require("node:child_process");
|
|
1759
|
+
const out = execSync(`lsof -t -nP -iTCP:${port} -sTCP:LISTEN`, { timeout: 3e3 }).toString().trim();
|
|
1760
|
+
return out.split("\n")[0] || null;
|
|
1683
1761
|
} catch {
|
|
1684
|
-
return
|
|
1762
|
+
return null;
|
|
1685
1763
|
}
|
|
1686
1764
|
}
|
|
1687
1765
|
async function listenWithFallback(server, cfg, host, onTwin) {
|
|
@@ -1713,27 +1791,27 @@ async function listenWithFallback(server, cfg, host, onTwin) {
|
|
|
1713
1791
|
return false;
|
|
1714
1792
|
};
|
|
1715
1793
|
if (await tryListen(cfg.port)) return cfg.port;
|
|
1716
|
-
|
|
1794
|
+
const running = await probeSameProject(cfg, cfg.port);
|
|
1795
|
+
if (running) {
|
|
1796
|
+
if (running.version && running.version !== pkgVersion()) {
|
|
1797
|
+
const pid2 = occupierPid(cfg.port);
|
|
1798
|
+
console.error(
|
|
1799
|
+
warn(`\u672C\u9879\u76EE\u5DF2\u6709\u4E00\u4E2A contactsheet \u5728 ${cfg.port} \u4E0A,\u4F46\u5B83\u662F v${running.version},\u5F53\u524D\u662F v${pkgVersion()}\u3002`) + `
|
|
1800
|
+
\u65E7\u5B9E\u4F8B\u4E0D\u4F1A\u81EA\u52A8\u5347\u7EA7 \u2014\u2014 \u5148\u505C\u6389\u5B83\u518D\u8D77:kill ${pid2 ?? "<lsof -t -iTCP:" + cfg.port + ">"}`
|
|
1801
|
+
);
|
|
1802
|
+
process.exit(1);
|
|
1803
|
+
}
|
|
1717
1804
|
console.log(
|
|
1718
1805
|
`${cyan("\u25CF")} \u672C\u9879\u76EE\u5DF2\u7ECF\u6709\u4E00\u4E2A contactsheet \u5728\u8DD1\u4E86,\u76F4\u63A5\u7528\u5B83:${link(`http://localhost:${cfg.port}/__cs`)}`
|
|
1719
1806
|
);
|
|
1720
1807
|
process.exit(0);
|
|
1721
1808
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
if (await tryListen(p)) {
|
|
1729
|
-
console.warn(
|
|
1730
|
-
warn(`\u7AEF\u53E3 ${cfg.port} \u88AB\u5360,\u5DF2\u987A\u5EF6\u5230 ${p} \u2192 `) + link(`http://localhost:${p}/__cs`) + "\n" + dim(` \u6CE8\u610F:\u6362\u7AEF\u53E3 = \u6362\u6D4F\u89C8\u5668\u6E90\u3002\u753B\u677F\u4F4D\u7F6E/\u6298\u53E0/\u5E95\u8272\u8FD9\u4E9B\u672C\u673A\u8BB0\u5FC6\u6309\u6E90\u9694\u79BB,\u8FD9\u4E2A\u7AEF\u53E3\u4E0B\u662F\u5168\u65B0\u7684;
|
|
1731
|
-
`) + dim(` .mcp.json \u4E0E hook \u4ECD\u6307\u5411 ${cfg.port}\u3002\u60F3\u8981\u7A33\u5B9A,\u8BF7\u91CA\u653E ${cfg.port},\u6216\u6539 config \u7684 port \u540E\u91CD\u8DD1 npx contactsheet init\u3002`)
|
|
1732
|
-
);
|
|
1733
|
-
return p;
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
throw new Error(`\u4ECE ${cfg.port} \u5F80\u4E0A\u8FDE\u8BD5 20 \u4E2A\u7AEF\u53E3\u90FD\u88AB\u5360,\u7528 --port \u6307\u5B9A\u4E00\u4E2A\u7A7A\u95F2\u7AEF\u53E3`);
|
|
1809
|
+
const pid = occupierPid(cfg.port);
|
|
1810
|
+
throw new Error(
|
|
1811
|
+
`\u7AEF\u53E3 ${cfg.port} \u88AB\u5176\u4ED6\u8FDB\u7A0B\u5360\u7528${pid ? `(PID ${pid})` : ""}\u3002.mcp.json/hook/config \u90FD\u6307\u5411 ${cfg.port},\u6362\u7AEF\u53E3\u5B83\u4EEC\u4F1A\u5168\u90E8\u5931\u8054,\u6240\u4EE5\u4E0D\u81EA\u52A8\u987A\u5EF6 \u2014\u2014
|
|
1812
|
+
\u91CA\u653E\u5B83:kill ${pid ?? `$(lsof -t -nP -iTCP:${cfg.port} -sTCP:LISTEN)`}
|
|
1813
|
+
\u6216\u6362\u7AEF\u53E3:\u6539 contactsheet.config.json \u7684 port \u540E\u91CD\u8DD1 npx contactsheet init(MCP/hook \u4F1A\u8DDF\u7740\u6362)`
|
|
1814
|
+
);
|
|
1737
1815
|
}
|
|
1738
1816
|
|
|
1739
1817
|
// src/watch/index.ts
|