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.
@@ -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-DMu8vPBF.js"></script>
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
- const child = await sidecar.start({ logPath: sidecarLogPath, force: false });
430
- // Wait briefly for it to bind :8008 so the homepage's poll flips to
431
- // "running" on the next tick.
432
- for (let i = 0; i < 20; i++) {
433
- if (await sidecar.probeExisting(PORT)) return { ok: true, pid: child?.pid || null };
434
- await new Promise((r) => setTimeout(r, 250));
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
- return { ok: true, pid: child?.pid || null, warning: "spawned but did not bind :8008 within 5s" };
437
- } catch (e) {
438
- return { ok: false, error: e.message };
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
- // Start bundled cicy-code daemon as a sidecar. Reuses an existing instance on
885
- // :8008 if one is already running (probeExisting → adopt, never double-spawn).
886
- cicyCodeSidecar
887
- .start({ logPath: path.join(os.homedir(), "logs", "cicy-code-sidecar.log") })
888
- .then((c) => { if (c) log.info(`[Sidecar] cicy-code spawned pid=${c.pid}`); })
889
- .catch((e) => log.warn(`[Sidecar] cicy-code start failed: ${e.message}`));
890
- startSidecarWatchdog();
891
-
892
- // Auto-register the local sidecar as 本地团队 once :8008 answers (主人: 一装好就要
893
- // 有占位卡)。addTeam upserts by host:port + auto-fills api_token, so re-runs are
894
- // no-ops. A fresh boot may seed/npm-pull the binary first, so probe up to ~90s.
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
- const sidecarPort = Number(process.env.CICY_CODE_PORT || 8008);
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.ok) log.info(`[Sidecar] local team ${r.upserted ? "refreshed" : "registered"} (${r.id})`);
903
- else log.warn(`[Sidecar] local team auto-register failed: ${r && r.error}`);
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] local team auto-register error: ${e.message}`); }
908
+ } catch (e) { log.warn(`[Sidecar] adopt probe error: ${e.message}`); }
907
909
  await new Promise((res) => setTimeout(res, 3000));
908
910
  }
909
- log.warn(`[Sidecar] local team auto-register gave up — :${sidecarPort} never came up`);
911
+ log.info(`[Sidecar] :${sidecarPort} 未在跑 占位卡显示「未运行」,等用户点「启动并打开」(带安装 drawer)`);
910
912
  })();
911
913
  }
912
914
 
@@ -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
- if (child && !force) return child;
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 —— colima VM 16G 机上把内存压垮,
98
- // jetsam 拿 SIGKILL 乱杀进程(cicy-code 被误伤)。所以 macOS/Linux 改回 native cicy-code
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
- // UNIFIED model (主人指令): run our OWN binary at ~/.local/bin/cicy-code on
109
- // every platform — never `npx cicy-code` (which reuses a stale globally-installed
110
- // copy and shadows updates). localbin seeds from the bundled subpackage on first
111
- // run (zero network) and uses npm ONLY as a download channel for updates; the run
112
- // path is always stable.
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
- // --helper removed (主人指令): Windows now runs cicy-code in normal mode (full
140
- // tmux-based multi-agent), same as mac/linux — no longer the single headless
141
- // 团队助手.
142
- const args = ["--desktop"];
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(exe, args, { stdio, detached, windowsHide: true, env });
148
- console.log(`[cicy-code-sidecar] spawned ${exe} ${args.join(" ")} pid=${child.pid} port=${port} detached=${detached} log=${logPath || "(none)"}`);
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
- setBusy("start");
2360
- toast.show({ id: opToastId, message: BUSY_LABEL.start, status: "running", progress: undefined });
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
  };