cicy-desktop 2.1.72 → 2.1.73

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.72",
3
+ "version": "2.1.73",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -112,9 +112,22 @@ async function start({ logPath, port = DEFAULT_PORT, force = false, version = nu
112
112
  if (!exe) {
113
113
  try { exe = (await localbin.ensure({ version }))?.exe; }
114
114
  catch (e) { console.warn(`[cicy-code-sidecar] localbin ensure failed: ${e.message}`); }
115
+ } else {
116
+ // Present — let ensure() do a zero-network bundle upgrade if cicy-desktop
117
+ // itself was updated and now ships a newer cicy-code (版本高了就更新).
118
+ try { await localbin.ensure({ version }); } catch {}
115
119
  }
116
120
  if (!exe) { console.warn("[cicy-code-sidecar] no cicy-code binary available"); return null; }
117
121
 
122
+ // cicy-desktop ALSO owns the mihomo binary (same npm/localbin model). Seed it
123
+ // into ~/.local/bin/mihomo from the bundle (zero network) BEFORE the cicy-code
124
+ // daemon boots, so cicy-code's own startup finds it already present and skips
125
+ // its GitHub/COS download. Best-effort — never block cicy-code on it.
126
+ try {
127
+ const r = await localbin.ensure({ name: "mihomo" });
128
+ if (r?.exe) console.log(`[cicy-code-sidecar] mihomo ready at ${r.exe} (v${r.version || "?"})`);
129
+ } catch (e) { console.warn(`[cicy-code-sidecar] mihomo seed skipped: ${e.message}`); }
130
+
118
131
  let stdio = ["ignore", "ignore", "ignore"];
119
132
  if (logPath) {
120
133
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
@@ -248,8 +261,14 @@ async function update({ logPath, port = DEFAULT_PORT, emit } = {}) {
248
261
  const localbin = require("./localbin");
249
262
  try {
250
263
  e({ phase: "download", status: "running", message: "检查最新版本…" });
264
+ const cur = localbin.currentVersion();
251
265
  const latest = await localbin.latestVersion();
252
266
  if (!latest) throw new Error("无法获取最新版本号");
267
+ if (cur && localbin.cmpVer(latest, cur) <= 0) {
268
+ // Already current — no download, no restart (不重复下载/更新).
269
+ e({ phase: "done", status: "done", message: `已是最新 ${cur}` });
270
+ return null;
271
+ }
253
272
  await localbin.fetchToLocalBin(latest, { emit }); // download → ~/.local/bin → re-link
254
273
  e({ phase: "swap", status: "running", message: `切换到 ${latest},启动…` });
255
274
  await stop({ port });
@@ -1,35 +1,120 @@
1
- // ~/.local/bin install model for the cicy-code daemon binary.
1
+ // ~/.local/bin install model for the binaries cicy-desktop OWNS.
2
2
  //
3
- // 主人指令 (2026-06): cicy-desktop OWNS the binary. It is bundled per-platform
4
- // (an optionalDependency of cicy-desktop). On first run we copy the bundled,
5
- // version-named binary into ~/.local/bin/cicy-code-<ver>-<plat> and point
6
- // ~/.local/bin/cicy-code at it (symlink on mac/linux; a plain COPY on Windows —
7
- // symlink/junction perms there are a minefield). The daemon is ALWAYS run from
8
- // that stable ~/.local/bin/cicy-code path — never `npx cicy-code`, which would
9
- // reuse a stale globally-installed copy and shadow updates.
3
+ // 主人指令 (2026-06): cicy-desktop OWNS its runtime binaries and distributes
4
+ // them THROUGH npm one channel for everything. Today that's two components:
5
+ //
6
+ // cicy-code ← npm cicy-code-<plat> → ~/.local/bin/cicy-code
7
+ // mihomo ← npm cicy-mihomo-<plat> → ~/.local/bin/mihomo
8
+ //
9
+ // Each is bundled per-platform as an optionalDependency of cicy-desktop. On
10
+ // first run we copy the bundled, version-named binary into
11
+ // ~/.local/bin/<base>-<ver>-<plat> and point ~/.local/bin/<base> at it
12
+ // (symlink on mac/linux; a plain COPY on Windows — symlink/junction perms there
13
+ // are a minefield). The binary is ALWAYS run from that stable ~/.local/bin path
14
+ // — never `npx`, which would reuse a stale globally-installed copy and shadow
15
+ // updates.
16
+ //
17
+ // Semantics (主人指令): 有就不装、没有就装、版本高了就更新、不重复下载/更新.
18
+ // - present & current → reuse, no work, no network
19
+ // - absent → seed from the bundle (zero network)
20
+ // - bundle newer than link → re-seed from the bundle (zero network upgrade,
21
+ // e.g. after cicy-desktop itself updated)
22
+ // - explicit update() → npm `pack` the latest per-platform subpackage,
23
+ // ONLY when the registry is actually ahead.
10
24
  //
11
25
  // Updates use npm ONLY as a download channel: `npm pack` the per-platform
12
26
  // subpackage (sha512-verified), extract the binary, copy it in as a NEW
13
- // version-named file, then re-point the cicy-code link (re-copy on Windows).
27
+ // version-named file, then re-point the link (re-copy on Windows). A version
28
+ // manifest at ~/.local/bin/.cicy-localbin.json records what each link points
29
+ // at, so version checks work cross-platform (including the Windows copy, which
30
+ // has no symlink target to parse).
14
31
 
15
32
  const fs = require("fs");
16
33
  const os = require("os");
17
34
  const path = require("path");
18
- const { execFile } = require("child_process");
35
+ const { execFile, execFileSync } = require("child_process");
19
36
 
20
37
  const IS_WIN = process.platform === "win32";
21
38
  const REGISTRY = process.env.CICY_NPM_REGISTRY || "https://registry.npmmirror.com";
22
39
  const LOCAL_BIN = path.join(os.homedir(), ".local", "bin");
40
+ const MANIFEST = path.join(LOCAL_BIN, ".cicy-localbin.json");
41
+
42
+ // The binaries we own. `pkgPrefix` + platform = the npm subpackage name;
43
+ // `base` is the stable link/file base name. The default component everywhere is
44
+ // cicy-code, so the legacy single-component callers keep working unchanged.
45
+ const COMPONENTS = {
46
+ "cicy-code": { pkgPrefix: "cicy-code", base: "cicy-code" },
47
+ "mihomo": { pkgPrefix: "cicy-mihomo", base: "mihomo" },
48
+ };
49
+ const DEFAULT = "cicy-code";
50
+ function comp(name) {
51
+ const c = COMPONENTS[name || DEFAULT];
52
+ if (!c) throw new Error(`unknown localbin component: ${name}`);
53
+ return c;
54
+ }
23
55
 
24
56
  function plat() {
25
57
  const osStr = IS_WIN ? "windows" : process.platform === "darwin" ? "darwin" : "linux";
26
58
  const arch = process.arch === "arm64" ? "arm64" : "x64";
27
59
  return `${osStr}-${arch}`;
28
60
  }
29
- const PKG = () => `cicy-code-${plat()}`;
30
- const BIN = IS_WIN ? "cicy-code.exe" : "cicy-code";
31
- const LINK = path.join(LOCAL_BIN, IS_WIN ? "cicy-code.exe" : "cicy-code");
32
- const versioned = (ver) => path.join(LOCAL_BIN, `cicy-code-${ver}-${plat()}${IS_WIN ? ".exe" : ""}`);
61
+ const pkgFor = (name) => `${comp(name).pkgPrefix}-${plat()}`;
62
+ const binFor = (name) => comp(name).base + (IS_WIN ? ".exe" : "");
63
+ const linkFor = (name) => path.join(LOCAL_BIN, binFor(name));
64
+ const versionedFor = (name, ver) =>
65
+ path.join(LOCAL_BIN, `${comp(name).base}-${ver}-${plat()}${IS_WIN ? ".exe" : ""}`);
66
+
67
+ // Legacy aliases (cicy-code is the default component).
68
+ const BIN = binFor(DEFAULT);
69
+ const LINK = linkFor(DEFAULT);
70
+ const versioned = (ver, name = DEFAULT) => versionedFor(name, ver);
71
+
72
+ // ── version helpers ───────────────────────────────────────────────────────
73
+ // Normalize "v1.10.3" / "1.10.3" → [1,10,3]; compare numerically.
74
+ function parseVer(v) {
75
+ return String(v || "").replace(/^v/i, "").split(/[.\-+]/).map((n) => parseInt(n, 10) || 0);
76
+ }
77
+ function cmpVer(a, b) {
78
+ const pa = parseVer(a), pb = parseVer(b);
79
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
80
+ const d = (pa[i] || 0) - (pb[i] || 0);
81
+ if (d) return d > 0 ? 1 : -1;
82
+ }
83
+ return 0;
84
+ }
85
+
86
+ function readManifest() {
87
+ try { return JSON.parse(fs.readFileSync(MANIFEST, "utf8")) || {}; } catch { return {}; }
88
+ }
89
+ function writeManifest(name, ver) {
90
+ const m = readManifest();
91
+ m[name] = ver;
92
+ try { fs.mkdirSync(LOCAL_BIN, { recursive: true }); fs.writeFileSync(MANIFEST, JSON.stringify(m, null, 2) + "\n"); } catch {}
93
+ }
94
+
95
+ // Best-effort: run the binary to read its version (fallback when the manifest
96
+ // has no record — e.g. a binary installed by the old GitHub/COS path).
97
+ function probeVersion(name) {
98
+ const link = linkFor(name);
99
+ if (!fs.existsSync(link)) return null;
100
+ const flag = name === "mihomo" ? "-v" : "--version";
101
+ try {
102
+ const out = execFileSync(link, [flag], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
103
+ const m = out.match(/\bv?(\d+\.\d+\.\d+)\b/);
104
+ return m ? m[1] : null;
105
+ } catch { return null; }
106
+ }
107
+
108
+ // The version the ~/.local/bin link currently points at: manifest first (fast,
109
+ // cross-platform), else probe the binary, else null.
110
+ function currentVersion(name = DEFAULT) {
111
+ if (!fs.existsSync(linkFor(name))) return null;
112
+ const m = readManifest();
113
+ if (m[name]) return m[name];
114
+ const probed = probeVersion(name);
115
+ if (probed) { writeManifest(name, probed); return probed; }
116
+ return null;
117
+ }
33
118
 
34
119
  function npmExec(args, timeout = 600000) {
35
120
  return new Promise((resolve, reject) => {
@@ -39,95 +124,145 @@ function npmExec(args, timeout = 600000) {
39
124
  }
40
125
 
41
126
  // Latest published version of the per-platform subpackage.
42
- async function latestVersion() {
43
- return (await npmExec(["view", PKG(), "version", `--registry=${REGISTRY}`], 30000)).trim();
127
+ async function latestVersion(name = DEFAULT) {
128
+ return (await npmExec(["view", pkgFor(name), "version", `--registry=${REGISTRY}`], 30000)).trim();
44
129
  }
45
130
 
46
- // Point ~/.local/bin/cicy-code at a version-named binary: symlink on POSIX, a
47
- // plain copy on Windows.
48
- function linkTo(verBinPath) {
131
+ // Point ~/.local/bin/<base> at a version-named binary: symlink on POSIX, a plain
132
+ // copy on Windows. Records the version in the manifest.
133
+ function linkTo(name, verBinPath, ver) {
134
+ const link = linkFor(name);
49
135
  fs.mkdirSync(LOCAL_BIN, { recursive: true });
50
- try { fs.rmSync(LINK, { force: true }); } catch {}
51
- if (IS_WIN) {
52
- fs.copyFileSync(verBinPath, LINK);
53
- } else {
54
- fs.symlinkSync(verBinPath, LINK);
55
- }
56
- return LINK;
136
+ try { fs.rmSync(link, { force: true }); } catch {}
137
+ if (IS_WIN) fs.copyFileSync(verBinPath, link);
138
+ else fs.symlinkSync(verBinPath, link);
139
+ if (ver) writeManifest(name, ver);
140
+ return link;
57
141
  }
58
142
 
59
- function placeBinary(srcBin, ver) {
143
+ function placeBinary(name, srcBin, ver) {
60
144
  if (!fs.existsSync(srcBin)) throw new Error(`source binary missing: ${srcBin}`);
61
145
  fs.mkdirSync(LOCAL_BIN, { recursive: true });
62
- const dst = versioned(ver);
146
+ const dst = versionedFor(name, ver);
63
147
  fs.copyFileSync(srcBin, dst);
64
148
  if (!IS_WIN) fs.chmodSync(dst, 0o755);
65
- linkTo(dst);
66
- return { exe: LINK, target: dst, version: ver };
149
+ linkTo(name, dst, ver);
150
+ return { exe: linkFor(name), target: dst, version: ver };
67
151
  }
68
152
 
69
153
  // The bundled per-platform subpackage shipped inside cicy-desktop (zero network).
70
- function bundledPkgDir() {
154
+ function bundledPkgDir(name) {
155
+ const pkg = pkgFor(name);
71
156
  const candidates = [
72
- path.join(__dirname, "..", "..", "node_modules", PKG()), // npm install layout
73
- path.join(process.resourcesPath || "", "runtime-pkgs", PKG()), // packaged (NSIS/dmg) layout
157
+ path.join(__dirname, "..", "..", "node_modules", pkg), // npm install layout
158
+ path.join(process.resourcesPath || "", "runtime-pkgs", pkg), // packaged (NSIS/dmg) layout
74
159
  ];
75
160
  for (const p of candidates) {
76
161
  try { if (fs.existsSync(path.join(p, "package.json"))) return p; } catch {}
77
162
  }
78
163
  return null;
79
164
  }
165
+ function bundledVersion(name) {
166
+ const dir = bundledPkgDir(name);
167
+ if (!dir) return null;
168
+ try { return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).version || null; } catch { return null; }
169
+ }
80
170
 
81
- // Install the binary from the bundled subpackage. null when not bundled.
82
- function fromBundle() {
83
- const dir = bundledPkgDir();
171
+ // Install/upgrade the binary from the bundled subpackage (zero network). Returns
172
+ // {exe,version} or null when not bundled. Reuses the existing link when it is
173
+ // already at the bundle version or newer (有就不装).
174
+ function fromBundle(name = DEFAULT) {
175
+ const dir = bundledPkgDir(name);
84
176
  if (!dir) return null;
85
- let ver;
86
- try { ver = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).version; } catch { return null; }
87
- const src = path.join(dir, BIN);
177
+ const ver = bundledVersion(name);
178
+ if (!ver) return null;
179
+ const src = path.join(dir, binFor(name));
88
180
  if (!fs.existsSync(src)) return null;
89
- if (fs.existsSync(versioned(ver))) { linkTo(versioned(ver)); return { exe: LINK, version: ver }; }
90
- return placeBinary(src, ver);
181
+ const cur = currentVersion(name);
182
+ if (cur && currentLink(name) && cmpVer(cur, ver) >= 0) return { exe: linkFor(name), version: cur };
183
+ if (fs.existsSync(versionedFor(name, ver))) { linkTo(name, versionedFor(name, ver), ver); return { exe: linkFor(name), version: ver }; }
184
+ return placeBinary(name, src, ver);
91
185
  }
92
186
 
93
187
  // Download <pkg>@<ver> via `npm pack` and install it into ~/.local/bin. npm is
94
188
  // ONLY the download channel — we copy the binary out and run it from ~/.local/bin.
95
- async function fetchToLocalBin(ver, { emit } = {}) {
189
+ // No-op download when that version is already on disk (不重复下载).
190
+ async function fetchToLocalBin(ver, { emit, name = DEFAULT } = {}) {
96
191
  const e = emit || (() => {});
97
- if (fs.existsSync(versioned(ver))) { linkTo(versioned(ver)); return { exe: LINK, version: ver }; }
98
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "cicy-cc-"));
192
+ if (fs.existsSync(versionedFor(name, ver))) { linkTo(name, versionedFor(name, ver), ver); return { exe: linkFor(name), version: ver }; }
193
+ const label = comp(name).base;
194
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "cicy-lb-"));
99
195
  try {
100
- e({ phase: "download", status: "running", message: `下载 cicy-code ${ver}…` });
101
- const out = await npmExec(["pack", `${PKG()}@${ver}`, `--registry=${REGISTRY}`, "--pack-destination", tmp]);
196
+ e({ phase: "download", status: "running", message: `下载 ${label} ${ver}…` });
197
+ const out = await npmExec(["pack", `${pkgFor(name)}@${ver}`, `--registry=${REGISTRY}`, "--pack-destination", tmp]);
102
198
  const tgz = path.join(tmp, out.trim().split("\n").pop().trim());
103
199
  await new Promise((resolve, reject) =>
104
200
  execFile("tar", ["-xzf", tgz, "-C", tmp], { windowsHide: true, timeout: 120000 }, (err) => (err ? reject(err) : resolve())));
105
- const res = placeBinary(path.join(tmp, "package", BIN), ver);
106
- e({ phase: "download", status: "done", message: `cicy-code ${ver} 就绪` });
201
+ const res = placeBinary(name, path.join(tmp, "package", binFor(name)), ver);
202
+ e({ phase: "download", status: "done", message: `${label} ${ver} 就绪` });
107
203
  return res;
108
204
  } finally {
109
205
  fs.rmSync(tmp, { recursive: true, force: true });
110
206
  }
111
207
  }
112
208
 
113
- // ~/.local/bin/cicy-code, if it exists.
114
- function currentLink() {
115
- return fs.existsSync(LINK) ? LINK : null;
209
+ // ~/.local/bin/<base>, if it exists.
210
+ function currentLink(name = DEFAULT) {
211
+ return fs.existsSync(linkFor(name)) ? linkFor(name) : null;
116
212
  }
117
213
 
118
- // Ensure ~/.local/bin/cicy-code exists and points at a usable binary.
119
- // - already linked → reuse (unless force)
120
- // - else bundled subpackage (zero network, the "pre-installed" path)
121
- // - else download latest (or a pinned version) via npm
122
- async function ensure({ version = null, force = false, emit = null } = {}) {
123
- if (!force && currentLink()) return { exe: LINK };
214
+ // Ensure ~/.local/bin/<base> exists and points at a usable binary, version-aware
215
+ // and network-frugal:
216
+ // - present & no newer bundle reuse as-is (有就不装, zero network)
217
+ // - present & bundle is newer re-seed from the bundle (zero network upgrade)
218
+ // - pinned version reuse if already that version, else fetch it
219
+ // - absent → seed from the bundle, else fetch latest from npm
220
+ async function ensure({ name = DEFAULT, version = null, force = false, emit = null } = {}) {
124
221
  const pin = version && version !== "latest" ? version : null;
125
- if (!force && !pin) {
126
- const b = fromBundle();
127
- if (b) return b;
222
+
223
+ if (pin) {
224
+ const cur = currentVersion(name);
225
+ if (!force && cur && cmpVer(cur, pin) === 0) return { exe: linkFor(name), version: cur };
226
+ if (fs.existsSync(versionedFor(name, pin))) { linkTo(name, versionedFor(name, pin), pin); return { exe: linkFor(name), version: pin }; }
227
+ return fetchToLocalBin(pin, { emit, name });
228
+ }
229
+
230
+ if (!force && currentLink(name)) {
231
+ // Present — only touch it if the bundle ships something newer (zero network).
232
+ const cur = currentVersion(name);
233
+ const bv = bundledVersion(name);
234
+ if (bv && (!cur || cmpVer(bv, cur) > 0)) {
235
+ const b = fromBundle(name);
236
+ if (b) return b;
237
+ }
238
+ return { exe: linkFor(name), version: cur };
239
+ }
240
+
241
+ // Absent (or forced): prefer the zero-network bundle seed, else npm latest.
242
+ const b = fromBundle(name);
243
+ if (b) return b;
244
+ const ver = await latestVersion(name);
245
+ return fetchToLocalBin(ver, { emit, name });
246
+ }
247
+
248
+ // On-demand update from the registry (the 更新 button). Fetches the latest only
249
+ // when the registry is actually ahead of what's installed (不重复下载/更新).
250
+ async function update({ name = DEFAULT, emit } = {}) {
251
+ const e = emit || (() => {});
252
+ const cur = currentVersion(name);
253
+ const latest = await latestVersion(name);
254
+ if (!latest) throw new Error("无法获取最新版本号");
255
+ if (cur && cmpVer(latest, cur) <= 0) {
256
+ e({ phase: "done", status: "done", message: `已是最新 ${cur}` });
257
+ return { exe: linkFor(name), version: cur, updated: false };
128
258
  }
129
- const ver = pin || (await latestVersion());
130
- return fetchToLocalBin(ver, { emit });
259
+ const res = await fetchToLocalBin(latest, { emit, name });
260
+ return { ...res, updated: true };
131
261
  }
132
262
 
133
- module.exports = { LOCAL_BIN, LINK, plat, versioned, latestVersion, fromBundle, fetchToLocalBin, currentLink, ensure };
263
+ module.exports = {
264
+ LOCAL_BIN, LINK, BIN, COMPONENTS, plat,
265
+ pkgFor, binFor, linkFor, versionedFor, versioned,
266
+ cmpVer, currentVersion, latestVersion,
267
+ fromBundle, bundledVersion, fetchToLocalBin, currentLink, ensure, update,
268
+ };
@@ -927,6 +927,10 @@ body {
927
927
 
928
928
  /* 首启门控条款页 (合规第一道整体同意) */
929
929
  .terms-gate { display: flex; align-items: center; justify-content: center; padding: 24px; }
930
+ /* .shell sets -webkit-app-region: drag; only .shell--app un-drags itself. The
931
+ terms gate / splash screens are bare .shell, so without this the whole panel
932
+ is a window-drag region and its buttons can't be clicked. */
933
+ .terms-gate__panel, .terms-gate__panel * { -webkit-app-region: no-drag; }
930
934
  .terms-gate__panel {
931
935
  position: relative; z-index: 1; width: min(680px, 94vw); max-height: 90vh;
932
936
  display: flex; flex-direction: column;