cicy-desktop 2.1.185 → 2.1.187
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/package.json +1 -1
- package/src/backends/homepage-react/assets/index-lEii78ll.js +377 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/sidecar-ipc.js +32 -10
- package/src/main.js +20 -18
- package/src/sidecar/cicy-code.js +164 -36
- package/workers/render/src/App.jsx +25 -13
- package/src/backends/homepage-react/assets/index-DMu8vPBF.js +0 -377
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
7
7
|
<link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
|
|
8
8
|
<title>CiCy Desktop</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-lEii78ll.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-CyUwy5Um.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -423,19 +423,41 @@ function register({ sidecarLogPath } = {}) {
|
|
|
423
423
|
|
|
424
424
|
// Start (or reuse) the cicy-code daemon. probeExisting inside start() reuses
|
|
425
425
|
// a healthy :8008; otherwise it spawns `npx cicy-code` / the Docker container.
|
|
426
|
-
ipcMain.handle("sidecar:start", async () => {
|
|
426
|
+
ipcMain.handle("sidecar:start", async (e) => {
|
|
427
|
+
// 流式把安装进度推给抽屉(主人: "为什么不弹 drawer 显示安装日志,卡在哪我怎么知道"):
|
|
428
|
+
// - ensureNode 的 Node 下载进度 → 走 start({emit})
|
|
429
|
+
// - npx cicy-code + cicy-code 首次 `brew install tmux` 装依赖的输出 → 都写在 sidecarLogPath,
|
|
430
|
+
// 这里 tail 这个文件、逐行 emit,用户就能看见在装啥、卡在哪。
|
|
431
|
+
const emit = (ev) => { try { e.sender.send("sidecar:op-progress", { op: "start", ...ev }); } catch {} };
|
|
427
432
|
try {
|
|
428
433
|
if (await sidecar.probeExisting(PORT)) return { ok: true, alreadyRunning: true };
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
434
|
+
let pos = 0; try { pos = fs.statSync(sidecarLogPath).size; } catch {}
|
|
435
|
+
const tail = setInterval(() => {
|
|
436
|
+
try {
|
|
437
|
+
const sz = fs.statSync(sidecarLogPath).size;
|
|
438
|
+
if (sz > pos) {
|
|
439
|
+
const buf = Buffer.alloc(sz - pos);
|
|
440
|
+
const fd = fs.openSync(sidecarLogPath, "r"); fs.readSync(fd, buf, 0, sz - pos, pos); fs.closeSync(fd);
|
|
441
|
+
pos = sz;
|
|
442
|
+
for (const line of buf.toString("utf8").split(/\r?\n/)) { const m = line.trim(); if (m) emit({ phase: "download", status: "running", message: m.slice(0, 240) }); }
|
|
443
|
+
}
|
|
444
|
+
} catch {}
|
|
445
|
+
}, 500);
|
|
446
|
+
const child = await sidecar.start({ logPath: sidecarLogPath, force: false, emit });
|
|
447
|
+
// Node 下载 + cicy-code 首次 brew 装依赖很慢 → 等到 ~6 分钟;子进程退出(失败)立即停手。
|
|
448
|
+
let up = false;
|
|
449
|
+
for (let i = 0; i < 720; i++) {
|
|
450
|
+
if (await sidecar.probeExisting(PORT)) { up = true; break; }
|
|
451
|
+
if (child && child.exitCode != null) break;
|
|
452
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
435
453
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return { ok: false, error:
|
|
454
|
+
clearInterval(tail);
|
|
455
|
+
if (up) { emit({ phase: "done", status: "done", message: "cicy-code 已就绪 :8008" }); return { ok: true, pid: child?.pid || null }; }
|
|
456
|
+
if (child && child.exitCode != null) { emit({ phase: "done", status: "error", message: `cicy-code 启动失败(exit=${child.exitCode}),见上方日志` }); return { ok: false, error: `cicy-code exited (${child.exitCode}) — 见安装日志` }; }
|
|
457
|
+
return { ok: true, pid: child?.pid || null, warning: "6 分钟内未就绪(可能还在装依赖,见日志)" };
|
|
458
|
+
} catch (err) {
|
|
459
|
+
emit({ phase: "done", status: "error", message: `启动失败:${err.message}` });
|
|
460
|
+
return { ok: false, error: err.message };
|
|
439
461
|
}
|
|
440
462
|
});
|
|
441
463
|
|
package/src/main.js
CHANGED
|
@@ -881,32 +881,34 @@ electronApp.whenReady().then(async () => {
|
|
|
881
881
|
// colima VM 在 16G mac 上把内存压垮被 jetsam SIGKILL。Windows 仍走 docker(WSL :8009,
|
|
882
882
|
// 由 sidecar-ipc 管),不在这里起 native。
|
|
883
883
|
if (process.platform !== "win32") {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
//
|
|
884
|
+
const sidecarPort = Number(process.env.CICY_CODE_PORT || 8008);
|
|
885
|
+
const lt = require("./backends/local-teams");
|
|
886
|
+
|
|
887
|
+
// 占位卡(主人: "先有一个 :8008 占位"): 开机**立刻无条件**注册本地团队,homepage 永远有
|
|
888
|
+
// 这张卡 —— :8008 没起来时显示「未运行」+ 启动/安装入口(不会再空首页);起来后下面的
|
|
889
|
+
// probe 循环 upsert 一次,状态翻 running + 触发 gateway key/teamId 注入。addTeam 幂等。
|
|
890
|
+
lt.addTeam({ base_url: `http://127.0.0.1:${sidecarPort}`, name: i18n.t("localTeams.defaultName") })
|
|
891
|
+
.then((r) => { if (r && r.id) log.info(`[Sidecar] local team placeholder registered (${r.id})`); })
|
|
892
|
+
.catch((e) => log.warn(`[Sidecar] placeholder register failed: ${e.message}`));
|
|
893
|
+
|
|
894
|
+
// **不在 boot 静默 start**(主人 bug 修复): 首次启动要装 Node/brew/tmux 等依赖,必须让
|
|
895
|
+
// 用户在点「启动并打开」时**看着 drawer 装**(命令/进度/错误/重试)。boot 静默装会:① 用户
|
|
896
|
+
// 看不到卡在哪 ② 和用户点击重复 spawn 抢锁。所以这里只:① 注册占位卡 ② 若 cicy-code 已在
|
|
897
|
+
// 跑(上次保活留下的)就 adopt 刷新状态 + 拿 key/teamId。真正的启动/安装由 sidecar:start
|
|
898
|
+
// (点「启动并打开」)驱动,带 drawer。
|
|
895
899
|
(async () => {
|
|
896
|
-
|
|
897
|
-
const lt = require("./backends/local-teams");
|
|
898
|
-
for (let i = 0; i < 30; i++) {
|
|
900
|
+
for (let i = 0; i < 40; i++) {
|
|
899
901
|
try {
|
|
900
902
|
if (await cicyCodeSidecar.probeExisting(sidecarPort)) {
|
|
901
903
|
const r = await lt.addTeam({ base_url: `http://127.0.0.1:${sidecarPort}`, name: i18n.t("localTeams.defaultName") });
|
|
902
|
-
if (r && r.
|
|
903
|
-
|
|
904
|
+
if (r && r.id) log.info(`[Sidecar] adopted running cicy-code → local team ready (${r.id})`);
|
|
905
|
+
startSidecarWatchdog(); // 已在跑 → 起 watchdog 保活(崩了重起,此时依赖已装、起得快)
|
|
904
906
|
return;
|
|
905
907
|
}
|
|
906
|
-
} catch (e) { log.warn(`[Sidecar]
|
|
908
|
+
} catch (e) { log.warn(`[Sidecar] adopt probe error: ${e.message}`); }
|
|
907
909
|
await new Promise((res) => setTimeout(res, 3000));
|
|
908
910
|
}
|
|
909
|
-
log.
|
|
911
|
+
log.info(`[Sidecar] :${sidecarPort} 未在跑 — 占位卡显示「未运行」,等用户点「启动并打开」(带安装 drawer)`);
|
|
910
912
|
})();
|
|
911
913
|
}
|
|
912
914
|
|
package/src/sidecar/cicy-code.js
CHANGED
|
@@ -18,7 +18,140 @@ const os = require("os");
|
|
|
18
18
|
const http = require("http");
|
|
19
19
|
const net = require("net");
|
|
20
20
|
const path = require("path");
|
|
21
|
-
const { spawn, execFileSync } = require("child_process");
|
|
21
|
+
const { spawn, execFileSync, execFile } = require("child_process");
|
|
22
|
+
|
|
23
|
+
// ── Node runtime bootstrap ───────────────────────────────────────────────────
|
|
24
|
+
// 主人(2026-06): native cicy-code 走 `npx cicy-code`,需要一个**可用**的 Node。但用户
|
|
25
|
+
// 机器可能没 node,或 node 太老(实测 josephs 是 node v13/npx6,老 npx 跑不动)。所以:
|
|
26
|
+
// 系统有 node≥20 就用;没有/太老 → 下载 Node 24 到 ~/cicy-ai/runtime/node(免 sudo,
|
|
27
|
+
// 用户自己拥有);下载也失败 → 提示用户去 nodejs.org 自己装。
|
|
28
|
+
const NODE_VER = process.env.CICY_NODE_VERSION || "v24.18.0";
|
|
29
|
+
const NODE_HOME = path.join(os.homedir(), "cicy-ai", "runtime", "node");
|
|
30
|
+
const NODE_SEARCH = ["/usr/local/bin", "/opt/homebrew/bin", path.join(os.homedir(), ".local", "bin"), path.join(NODE_HOME, "bin")];
|
|
31
|
+
function nodeMajor(bin) {
|
|
32
|
+
try { const m = String(execFileSync(bin, ["-v"], { encoding: "utf8", timeout: 5000 })).match(/v(\d+)\./); return m ? Number(m[1]) : 0; } catch { return 0; }
|
|
33
|
+
}
|
|
34
|
+
function findUsableNode() {
|
|
35
|
+
for (const d of NODE_SEARCH) {
|
|
36
|
+
const node = path.join(d, "node"), npx = path.join(d, "npx");
|
|
37
|
+
try { if (fs.existsSync(node) && fs.existsSync(npx) && nodeMajor(node) >= 20) return d; } catch {}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const pexec = (cmd, args, timeout) => new Promise((res, rej) => execFile(cmd, args, { timeout, windowsHide: true }, (e) => (e ? rej(e) : res())));
|
|
42
|
+
// Returns a bin dir with node+npx (node≥20), or null. emit streams progress to a drawer.
|
|
43
|
+
async function ensureNode({ emit } = {}) {
|
|
44
|
+
const e = emit || (() => {});
|
|
45
|
+
let dir = findUsableNode();
|
|
46
|
+
if (dir) return dir;
|
|
47
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
48
|
+
const fname = `node-${NODE_VER}-darwin-${arch}.tar.gz`;
|
|
49
|
+
const urls = [
|
|
50
|
+
`https://cdn.npmmirror.com/binaries/node/${NODE_VER}/${fname}`, // CN-fast
|
|
51
|
+
`https://nodejs.org/dist/${NODE_VER}/${fname}`, // 官方兜底
|
|
52
|
+
];
|
|
53
|
+
e({ phase: "node", status: "running", message: `未检测到可用 Node(系统无 node 或版本 <20),正在安装 Node ${NODE_VER}(约 50MB)…` });
|
|
54
|
+
fs.mkdirSync(NODE_HOME, { recursive: true });
|
|
55
|
+
const tmp = path.join(os.tmpdir(), `cicy-${fname}`);
|
|
56
|
+
for (const url of urls) {
|
|
57
|
+
try {
|
|
58
|
+
e({ phase: "node", status: "running", message: `$ curl -fL -o ${tmp} ${url}` });
|
|
59
|
+
await pexec("curl", ["-fL", "--retry", "2", "-o", tmp, url], 300000);
|
|
60
|
+
e({ phase: "node", status: "running", message: `$ tar -xzf <node> -C ${NODE_HOME} --strip-components 1` });
|
|
61
|
+
await pexec("tar", ["-xzf", tmp, "-C", NODE_HOME, "--strip-components", "1"], 120000);
|
|
62
|
+
if (fs.existsSync(path.join(NODE_HOME, "bin", "node")) && fs.existsSync(path.join(NODE_HOME, "bin", "npx"))) {
|
|
63
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
64
|
+
e({ phase: "node", status: "done", message: `Node ${NODE_VER} 安装完成 → ${path.join(NODE_HOME, "bin")}` });
|
|
65
|
+
return path.join(NODE_HOME, "bin");
|
|
66
|
+
}
|
|
67
|
+
e({ phase: "node", status: "running", message: `解压后没找到 node/npx,换下一个源…` });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn(`[cicy-code-sidecar] node install failed (${url}): ${err.message}`);
|
|
70
|
+
e({ phase: "node", status: "running", message: `Node 下载失败(${url.includes("npmmirror") ? "npmmirror" : "nodejs.org"}):${String(err.message).slice(0, 160)} — 换源重试…` });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
74
|
+
e({ phase: "node", status: "error", message: "❌ Node 自动安装失败(两个源都没成)—— 请打开 https://nodejs.org 下载安装 Node(选 LTS),装好后点「重试」" });
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── 网络环境探测(CN 用 npmmirror,海外用 npmjs)──────────────────────────────
|
|
79
|
+
// 主人(npx 执行前要看本机网络,CN 用 CN mirror): 探 generate_204,能 204(够到 Google/有代理)
|
|
80
|
+
// = 海外;超时/失败(GFW)= CN。探一次缓存。CICY_NPM_REGISTRY 覆盖时不探。
|
|
81
|
+
let _cnCache = null;
|
|
82
|
+
function probeIsCN() {
|
|
83
|
+
if (_cnCache !== null) return _cnCache;
|
|
84
|
+
_cnCache = new Promise((resolve) => {
|
|
85
|
+
let done = false; const fin = (cn) => { if (!done) { done = true; resolve(cn); } };
|
|
86
|
+
try {
|
|
87
|
+
const req = require("https").get("https://www.gstatic.com/generate_204", { timeout: 2500 }, (res) => { const ok = res.statusCode === 204; res.destroy(); fin(!ok); });
|
|
88
|
+
req.on("timeout", () => { req.destroy(); fin(true); });
|
|
89
|
+
req.on("error", () => fin(true));
|
|
90
|
+
} catch { fin(true); }
|
|
91
|
+
});
|
|
92
|
+
return _cnCache;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Homebrew + 系统依赖 ───────────────────────────────────────────────────────
|
|
96
|
+
const BREW_DIRS = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
97
|
+
function findBrew() { for (const d of BREW_DIRS) { const p = path.join(d, "brew"); try { if (fs.existsSync(p)) return p; } catch {} } return null; }
|
|
98
|
+
function cmdExists(name, pathEnv) {
|
|
99
|
+
try { execFileSync("bash", ["-lc", `command -v ${name} >/dev/null 2>&1`], { timeout: 8000, env: { ...process.env, PATH: pathEnv } }); return true; } catch { return false; }
|
|
100
|
+
}
|
|
101
|
+
// brew install <dep>,逐行把输出 emit 到 drawer。CN 用 USTC bottle 镜像加速。
|
|
102
|
+
function brewInstallStream(brew, dep, env, e) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
e({ phase: "deps", status: "running", message: `$ brew install ${dep}` });
|
|
105
|
+
let buf = "";
|
|
106
|
+
const ch = spawn(brew, ["install", dep], { env });
|
|
107
|
+
const pump = (b) => { buf += b.toString("utf8"); let nl; while ((nl = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, nl).replace(/\r$/, "").trim(); buf = buf.slice(nl + 1); if (line) e({ phase: "deps", status: "running", message: line.slice(0, 200) }); } };
|
|
108
|
+
ch.stdout.on("data", pump); ch.stderr.on("data", pump);
|
|
109
|
+
ch.on("close", (code) => resolve(code === 0));
|
|
110
|
+
ch.on("error", (err) => { e({ phase: "deps", status: "running", message: `brew install ${dep} 起不来: ${err.message}` }); resolve(false); });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 完整环境引导:CN 探测 → node24 → brew → brew 装 tmux/jq → mihomo(best-effort)。
|
|
115
|
+
// 返回 { nodeBinDir, registry } 或 null(失败,drawer 已 emit 原因 + 可重试)。
|
|
116
|
+
async function ensureEnv({ emit } = {}) {
|
|
117
|
+
const e = emit || (() => {});
|
|
118
|
+
// 1) 网络环境 → registry + bottle 镜像
|
|
119
|
+
const cn = process.env.CICY_NPM_REGISTRY ? true : await probeIsCN();
|
|
120
|
+
const registry = process.env.CICY_NPM_REGISTRY || (cn ? "https://registry.npmmirror.com" : "https://registry.npmjs.org");
|
|
121
|
+
e({ phase: "net", status: "running", message: `网络环境:${cn ? "国内(CN)→ 用 npmmirror + USTC bottle 镜像" : "海外 → 用 npmjs"}` });
|
|
122
|
+
|
|
123
|
+
// 2) Node(系统 ≥20 用,否则装 Node 24)
|
|
124
|
+
const nodeBinDir = await ensureNode({ emit });
|
|
125
|
+
if (!nodeBinDir) return null;
|
|
126
|
+
|
|
127
|
+
// 3+4) Homebrew + tmux/jq(cicy-code 跑 tmux 多 agent 必需,首次它自己装很慢且会和我们抢锁,
|
|
128
|
+
// 所以这里**预装好**,cicy-code 启动时已就绪)。
|
|
129
|
+
const pathEnv = `${nodeBinDir}:${BREW_DIRS.join(":")}:${NODE_SEARCH.join(":")}:/usr/bin:/bin:${process.env.PATH || ""}`;
|
|
130
|
+
const brew = findBrew();
|
|
131
|
+
if (!brew) {
|
|
132
|
+
e({ phase: "deps", status: "error", message: "❌ 未检测到 Homebrew(装 tmux 等依赖要用)。请在「终端」执行后点「重试」:\n/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" });
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const brewEnv = { ...process.env, PATH: pathEnv, HOMEBREW_NO_AUTO_UPDATE: "1", ...(cn ? { HOMEBREW_BOTTLE_DOMAIN: process.env.HOMEBREW_BOTTLE_DOMAIN || "https://mirrors.ustc.edu.cn/homebrew-bottles" } : {}) };
|
|
136
|
+
for (const dep of ["tmux", "jq"]) {
|
|
137
|
+
if (cmdExists(dep, pathEnv)) { e({ phase: "deps", status: "running", message: `✓ ${dep} 已安装` }); continue; }
|
|
138
|
+
e({ phase: "deps", status: "running", message: `安装依赖 ${dep}(可能要几分钟)…` });
|
|
139
|
+
const ok = await brewInstallStream(brew, dep, brewEnv, e);
|
|
140
|
+
if (!ok) { e({ phase: "deps", status: "error", message: `❌ brew install ${dep} 失败(见上方日志),点「重试」` }); return null; }
|
|
141
|
+
e({ phase: "deps", status: "running", message: `✓ ${dep} 安装完成` });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 5) mihomo(Chrome 代理用,best-effort —— 失败不挡 cicy-code 启动)
|
|
145
|
+
try {
|
|
146
|
+
if (!cmdExists("mihomo", pathEnv) && !cmdExists("cicy-mihomo", pathEnv)) {
|
|
147
|
+
e({ phase: "deps", status: "running", message: `$ npx -y cicy-mihomo install(后台,失败不影响启动)` });
|
|
148
|
+
const m = spawn(path.join(nodeBinDir, "npx"), ["-y", "cicy-mihomo", "install"], { env: { ...process.env, PATH: pathEnv, npm_config_registry: registry }, stdio: "ignore", detached: true });
|
|
149
|
+
m.unref();
|
|
150
|
+
}
|
|
151
|
+
} catch {}
|
|
152
|
+
|
|
153
|
+
return { nodeBinDir, registry };
|
|
154
|
+
}
|
|
22
155
|
|
|
23
156
|
const DEFAULT_PORT = Number(process.env.CICY_CODE_PORT || 8008);
|
|
24
157
|
|
|
@@ -91,13 +224,19 @@ async function startFromRuntime({ logPath, port }) {
|
|
|
91
224
|
return c;
|
|
92
225
|
}
|
|
93
226
|
|
|
94
|
-
async function start({ logPath, port = DEFAULT_PORT, force = false, version = null } = {}) {
|
|
95
|
-
|
|
227
|
+
async function start({ logPath, port = DEFAULT_PORT, force = false, version = null, emit = null } = {}) {
|
|
228
|
+
// **永不重复 spawn 活着的实例**(主人 bug 修复): cicy-code 首次启动要 `brew install tmux`
|
|
229
|
+
// 等依赖,要几分钟,这期间 :8008 还没 bind。watchdog(:8008 探不到)和用户点「启动」都会
|
|
230
|
+
// 再调 start() —— 如果再 spawn 一个,多个实例抢 brew tmux 的锁 → 全部「环境初始化失败」→
|
|
231
|
+
// :8008 永远起不来。所以:只要我们 spawn 的 child 进程还活着(没 exit),一律复用,绝不再
|
|
232
|
+
// spawn,连 force 也不行(update() 是先 stop() 杀掉 child 再 start,那时 child 已 null)。
|
|
233
|
+
if (child && child.exitCode == null && child.signalCode == null) {
|
|
234
|
+
console.log(`[cicy-code-sidecar] already running pid=${child.pid} (setup may be in progress) — reuse, no double-spawn`);
|
|
235
|
+
return child;
|
|
236
|
+
}
|
|
96
237
|
|
|
97
|
-
// 主人(2026-06 方向回调): mac 资源吃不消 docker
|
|
98
|
-
//
|
|
99
|
-
// (:8008,本机直接跑二进制,省掉 4–8G 的 VM)。Windows 仍走 docker(WSL :8009,由
|
|
100
|
-
// sidecar-ipc 管),native 一律不起。
|
|
238
|
+
// 主人(2026-06 方向回调): mac 资源吃不消 docker(colima VM 压垮内存被 jetsam 杀)→
|
|
239
|
+
// macOS/Linux 改回 native cicy-code(:8008,走 `npx cicy-code`)。Windows 仍走 docker。
|
|
101
240
|
if (process.platform === "win32") return null;
|
|
102
241
|
|
|
103
242
|
if (!force && await probeExisting(port)) {
|
|
@@ -105,47 +244,36 @@ async function start({ logPath, port = DEFAULT_PORT, force = false, version = nu
|
|
|
105
244
|
return null;
|
|
106
245
|
}
|
|
107
246
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const localbin = require("./localbin");
|
|
114
|
-
let exe = localbin.currentLink();
|
|
115
|
-
if (!exe) {
|
|
116
|
-
try { exe = (await localbin.ensure({ version }))?.exe; }
|
|
117
|
-
catch (e) { console.warn(`[cicy-code-sidecar] localbin ensure failed: ${e.message}`); }
|
|
118
|
-
} else {
|
|
119
|
-
// Present — let ensure() do a zero-network bundle upgrade if cicy-desktop
|
|
120
|
-
// itself was updated and now ships a newer cicy-code (版本高了就更新).
|
|
121
|
-
try { await localbin.ensure({ version }); } catch {}
|
|
122
|
-
}
|
|
123
|
-
if (!exe) { console.warn("[cicy-code-sidecar] no cicy-code binary available"); return null; }
|
|
124
|
-
|
|
125
|
-
// 主人: 不再 seed host mihomo —— docker-only 后 mihomo 只在容器里跑(host 不用)。
|
|
126
|
-
// 内置的 cicy-mihomo-<plat> 依赖已从 package.json 移除。
|
|
247
|
+
// 1) 完整环境引导(CN 探测 → node24 → brew → 预装 tmux/jq → mihomo),全程 emit 到 drawer。
|
|
248
|
+
// 预装好依赖,cicy-code 启动时已就绪、不再自己慢慢 brew 装、不再和我们抢锁。
|
|
249
|
+
const env0 = await ensureEnv({ emit });
|
|
250
|
+
if (!env0) { console.warn("[cicy-code-sidecar] ensureEnv failed — cannot start"); return null; }
|
|
251
|
+
const { nodeBinDir, registry } = env0;
|
|
127
252
|
|
|
253
|
+
// 2) `npx cicy-code` —— npm 按本机真实架构拉 cicy-code-<plat>,文件用户自己拥有(无跨架构/
|
|
254
|
+
// 权限坑)。用上面那个 Node 的 npx,并把它的 bin + brew 放 PATH 首位让 cicy-code 找到 tmux/jq。
|
|
128
255
|
let stdio = ["ignore", "ignore", "ignore"];
|
|
129
256
|
if (logPath) {
|
|
130
257
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
131
258
|
const fd = fs.openSync(logPath, "a");
|
|
132
259
|
stdio = ["ignore", fd, fd];
|
|
133
260
|
}
|
|
261
|
+
const npxAbs = path.join(nodeBinDir, "npx");
|
|
262
|
+
const childPath = `${nodeBinDir}:${BREW_DIRS.join(":")}:${NODE_SEARCH.join(":")}:/usr/bin:/bin:${process.env.PATH || ""}`;
|
|
134
263
|
const env = {
|
|
135
264
|
...process.env,
|
|
136
265
|
CICY_CODE_PORT: String(port),
|
|
137
266
|
PORT: String(port),
|
|
267
|
+
npm_config_registry: registry,
|
|
268
|
+
PATH: childPath, // 给 npx 自己找 node/npm
|
|
138
269
|
};
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// 退出保活(主人):mac/linux 把 native cicy-code spawn 成 detached + unref,让它脱离
|
|
144
|
-
// Electron 的进程组——退出 App 不带走 daemon(及其 tmux agent),下次启动 probeExisting
|
|
145
|
-
// 探到就 adopt。Windows 不走这条(docker)。
|
|
270
|
+
const spec = version ? `cicy-code@${version}`
|
|
271
|
+
: (process.env.CICY_CODE_VERSION ? `cicy-code@${process.env.CICY_CODE_VERSION}` : "cicy-code");
|
|
272
|
+
emit && emit({ phase: "cicy-code", status: "running", message: "启动 cicy-code(首次会下载 + 装依赖,请稍候)…" });
|
|
273
|
+
// 退出保活(主人):detached + unref → 关 App 不带走 daemon(及其 tmux agent),下次 adopt。
|
|
146
274
|
const detached = process.platform !== "win32";
|
|
147
|
-
child = spawn(
|
|
148
|
-
console.log(`[cicy-code-sidecar] spawned ${
|
|
275
|
+
child = spawn(npxAbs, ["-y", spec], { stdio, detached, windowsHide: true, env });
|
|
276
|
+
console.log(`[cicy-code-sidecar] spawned ${npxAbs} -y ${spec} pid=${child.pid} port=${port} registry=${registry} detached=${detached} log=${logPath || "(none)"}`);
|
|
149
277
|
if (detached) { try { child.unref(); } catch {} }
|
|
150
278
|
|
|
151
279
|
child.on("exit", (code, signal) => {
|
|
@@ -354,4 +482,4 @@ async function update({ logPath, port = DEFAULT_PORT, emit } = {}) {
|
|
|
354
482
|
}
|
|
355
483
|
}
|
|
356
484
|
|
|
357
|
-
module.exports = { start, stop, restart, update, probeExisting, clearNpxCache, isUpdating };
|
|
485
|
+
module.exports = { start, stop, restart, update, probeExisting, clearNpxCache, isUpdating, ensureEnv, ensureNode };
|
|
@@ -87,9 +87,9 @@ let drawerState = null; // null = closed
|
|
|
87
87
|
function emitDrawer() { drawerListeners.forEach((l) => l(drawerState)); }
|
|
88
88
|
function clockHHMMSS() { const d = new Date(); return d.toTimeString().slice(0, 8); }
|
|
89
89
|
const updateDrawer = {
|
|
90
|
-
open({ teamId, fromVer, toVer, onRetry } = {}) {
|
|
90
|
+
open({ teamId, fromVer, toVer, onRetry, title } = {}) {
|
|
91
91
|
drawerState = {
|
|
92
|
-
teamId, fromVer: fromVer || null, toVer: toVer || null,
|
|
92
|
+
teamId, title: title || null, fromVer: fromVer || null, toVer: toVer || null,
|
|
93
93
|
status: "running", // running | done | error
|
|
94
94
|
phase: "download", // download | swap | done
|
|
95
95
|
logs: [],
|
|
@@ -149,7 +149,7 @@ function UpdateDrawerHost() {
|
|
|
149
149
|
return (
|
|
150
150
|
<button type="button" className={`drawer-min drawer-min--${st.status}`} data-id="UpdateDrawer-restore" onClick={() => updateDrawer.restore()}>
|
|
151
151
|
<span className="drawer-min__spark">{running ? <Spinner /> : st.status === "done" ? "✓" : st.status === "reboot" ? "⟳" : "!"}</span>
|
|
152
|
-
<span className="drawer-min__label">{tr("updateDrawer.title", "更新 cicy-code")}{st.toVer ? ` · v${st.toVer}` : ""}</span>
|
|
152
|
+
<span className="drawer-min__label">{st.title || tr("updateDrawer.title", "更新 cicy-code")}{st.toVer ? ` · v${st.toVer}` : ""}</span>
|
|
153
153
|
</button>
|
|
154
154
|
);
|
|
155
155
|
}
|
|
@@ -162,7 +162,7 @@ function UpdateDrawerHost() {
|
|
|
162
162
|
{running ? <Spinner /> : st.status === "done" ? "✓" : st.status === "reboot" ? "⟳" : "!"}
|
|
163
163
|
</span>
|
|
164
164
|
<div>
|
|
165
|
-
<div className="drawer__h">{tr("updateDrawer.title", "更新 cicy-code")}</div>
|
|
165
|
+
<div className="drawer__h">{st.title || tr("updateDrawer.title", "更新 cicy-code")}</div>
|
|
166
166
|
<div className="drawer__sub">{st.fromVer ? `v${st.fromVer}` : tr("updateDrawer.current", "当前")} → {st.toVer ? `v${st.toVer}` : tr("updateDrawer.latest", "最新版")}</div>
|
|
167
167
|
</div>
|
|
168
168
|
</div>
|
|
@@ -2353,18 +2353,30 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
|
|
|
2353
2353
|
// creating the window — so we never pop a blank page that needs a manual
|
|
2354
2354
|
// reload. (/api/health is NOT used — it's unreliable mid-boot; the gate is a
|
|
2355
2355
|
// raw TCP probe.) Remote/custom teams just open and show their own UI.
|
|
2356
|
+
// 启动本地 cicy-code,带**安装进度抽屉**(主人: 首次要装 Node + cicy-code 自己 brew 装
|
|
2357
|
+
// tmux 依赖,几分钟,必须让用户看见执行什么命令/卡在哪/出什么错,且能重试)。
|
|
2358
|
+
// sidecar:start 会流式 emit:Node 下载命令 + npx cicy-code + brew 装依赖的日志逐行推过来。
|
|
2359
|
+
const runStartFlow = async () => {
|
|
2360
|
+
setBusy("start");
|
|
2361
|
+
updateDrawer.open({ teamId: team.id, title: tr("sidecar.startTitle", "启动 cicy-code"), onRetry: runStartFlow });
|
|
2362
|
+
updateDrawer.push({ phase: "download", status: "running", message: tr("sidecar.starting", "启动 cicy-code(首次需安装 Node + 依赖,日志见下)…") });
|
|
2363
|
+
const unsub = window.cicy?.sidecar?.onOpProgress?.((ev) => updateDrawer.push(ev));
|
|
2364
|
+
let r;
|
|
2365
|
+
try { r = await window.cicy.sidecar.start(); } catch (e) { r = { ok: false, error: e?.message || String(e) }; }
|
|
2366
|
+
try { unsub?.(); } catch {}
|
|
2367
|
+
setBusy(""); onRefresh?.();
|
|
2368
|
+
if (!r?.ok || r?.warning) {
|
|
2369
|
+
updateDrawer.finish({ ok: false, message: tr("sidecar.startFailed", "启动失败") + (r?.error ? `: ${r.error}` : r?.warning ? `: ${r.warning}` : "") });
|
|
2370
|
+
return false;
|
|
2371
|
+
}
|
|
2372
|
+
updateDrawer.finish({ ok: true, message: tr("sidecar.startedOk", "cicy-code 已就绪") });
|
|
2373
|
+
return true;
|
|
2374
|
+
};
|
|
2356
2375
|
const handleOpen = async () => {
|
|
2357
2376
|
if (busy) return;
|
|
2358
2377
|
if (!running && local && window.cicy?.sidecar?.start) {
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
const r = await window.cicy.sidecar.start().catch((e) => ({ ok: false, error: e?.message || String(e) }));
|
|
2362
|
-
setBusy(""); onRefresh?.();
|
|
2363
|
-
if (!r?.ok || r?.warning) { // didn't come up — surface it, don't open a dead link
|
|
2364
|
-
toast.show({ id: opToastId, message: tr("sidecar.startFailed", "启动失败") + (r?.error ? `: ${r.error}` : r?.warning ? `: ${r.warning}` : ""), status: "error", ttl: 8000 });
|
|
2365
|
-
return;
|
|
2366
|
-
}
|
|
2367
|
-
toast.dismiss(opToastId); // came up — no lingering toast, the window opens
|
|
2378
|
+
const ok = await runStartFlow();
|
|
2379
|
+
if (!ok) return; // 没起来 — 抽屉里有日志 + 重试,不开死链
|
|
2368
2380
|
}
|
|
2369
2381
|
onOpen(); // openTeam() gates on list_windows + TCP liveness before showing
|
|
2370
2382
|
};
|