cicy-desktop 2.1.238 → 2.1.239
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.
|
|
3
|
+
"version": "2.1.239",
|
|
4
4
|
"description": "CiCy - AI-powered operating system browser",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -139,11 +139,11 @@
|
|
|
139
139
|
"//optionalDependencies": "(2026-06 回调): mac/linux 改回 native cicy-code(:8008,colima 在 16G mac 上压垮内存)→ 重新内置 cicy-code-<plat> / cicy-mihomo-<plat>,localbin.fromBundle 零网络 seed(npm 仅作更新通道,带 npmmirror→npmjs 回退)。这些由 scripts/sync-runtime-deps.cjs 在 tag-push 时同步到最新版。npm 的 os/cpu 字段保证每个平台只装自己那份。Windows 仍走 docker(WSL :8009),它那份 cicy-code-windows 用不到但 bundle 着无害。",
|
|
140
140
|
"optionalDependencies": {
|
|
141
141
|
"electron": "41.0.3",
|
|
142
|
-
"cicy-code-darwin-x64": "2.3.
|
|
143
|
-
"cicy-code-darwin-arm64": "2.3.
|
|
144
|
-
"cicy-code-linux-x64": "2.3.
|
|
145
|
-
"cicy-code-linux-arm64": "2.3.
|
|
146
|
-
"cicy-code-windows-x64": "2.3.
|
|
142
|
+
"cicy-code-darwin-x64": "2.3.194",
|
|
143
|
+
"cicy-code-darwin-arm64": "2.3.194",
|
|
144
|
+
"cicy-code-linux-x64": "2.3.194",
|
|
145
|
+
"cicy-code-linux-arm64": "2.3.194",
|
|
146
|
+
"cicy-code-windows-x64": "2.3.193",
|
|
147
147
|
"cicy-mihomo-darwin-x64": "1.10.4",
|
|
148
148
|
"cicy-mihomo-darwin-arm64": "1.10.4",
|
|
149
149
|
"cicy-mihomo-linux-x64": "1.10.4",
|
package/src/sidecar/cicy-code.js
CHANGED
|
@@ -28,11 +28,13 @@ const { t } = require("../i18n"); // 安装/升级日志走 i18n(main 进程按
|
|
|
28
28
|
// ── Node runtime bootstrap ───────────────────────────────────────────────────
|
|
29
29
|
// (2026-06): native cicy-code 走 `npx cicy-code`,需要一个**可用**的 Node。但用户
|
|
30
30
|
// 机器可能没 node,或 node 太老(实测 josephs 是 node v13/npx6,老 npx 跑不动)。所以:
|
|
31
|
-
// 系统有 node≥20 就用;没有/太老 → 下载 Node 24 到
|
|
32
|
-
//
|
|
31
|
+
// 系统有 node≥20 就用;没有/太老 → 下载 Node 24 到 ~/.local/node(免 sudo,用户自己
|
|
32
|
+
// 拥有);下载也失败 → 提示用户去 nodejs.org 自己装。老装在 ~/cicy-ai/runtime/node 的
|
|
33
|
+
// 树:ensureNode 首次同盘 rename 迁进 ~/.local/node,迁不动也能被 legacy 搜索路径命中。
|
|
33
34
|
const NODE_VER = process.env.CICY_NODE_VERSION || "v24.18.0";
|
|
34
|
-
const NODE_HOME = path.join(os.homedir(), "
|
|
35
|
-
const
|
|
35
|
+
const NODE_HOME = path.join(os.homedir(), ".local", "node");
|
|
36
|
+
const LEGACY_NODE_HOME = path.join(os.homedir(), "cicy-ai", "runtime", "node"); // read-only compat
|
|
37
|
+
const NODE_SEARCH = ["/usr/local/bin", "/opt/homebrew/bin", path.join(os.homedir(), ".local", "bin"), path.join(NODE_HOME, "bin"), path.join(LEGACY_NODE_HOME, "bin")];
|
|
36
38
|
function nodeMajor(bin) {
|
|
37
39
|
try { const m = String(execFileSync(bin, ["-v"], { encoding: "utf8", timeout: 5000 })).match(/v(\d+)\./); return m ? Number(m[1]) : 0; } catch { return 0; }
|
|
38
40
|
}
|
|
@@ -60,6 +62,15 @@ async function ensureNode({ emit } = {}) {
|
|
|
60
62
|
}
|
|
61
63
|
return dir;
|
|
62
64
|
};
|
|
65
|
+
// Migrate a legacy ~/cicy-ai/runtime/node tree into ~/.local/node — same-fs
|
|
66
|
+
// rename is instant. Cross-fs (EXDEV) → leave it; the legacy NODE_SEARCH dir
|
|
67
|
+
// still finds it. New downloads (below) land in ~/.local/node directly.
|
|
68
|
+
try {
|
|
69
|
+
if (!fs.existsSync(path.join(NODE_HOME, "bin", "node")) && fs.existsSync(path.join(LEGACY_NODE_HOME, "bin", "node"))) {
|
|
70
|
+
fs.mkdirSync(path.dirname(NODE_HOME), { recursive: true });
|
|
71
|
+
fs.renameSync(LEGACY_NODE_HOME, NODE_HOME);
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
63
74
|
let dir = findUsableNode();
|
|
64
75
|
if (dir) return adopt(dir);
|
|
65
76
|
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
@@ -188,7 +199,10 @@ async function ensureEnv({ emit } = {}) {
|
|
|
188
199
|
let mihomoBin = null;
|
|
189
200
|
try {
|
|
190
201
|
const runtime = require("./runtime");
|
|
191
|
-
|
|
202
|
+
// ensureFromBundle migrates a legacy ~/cicy-ai/runtime mihomo into ~/.local/bin
|
|
203
|
+
// (no network) and fixes the symlink; falls through to the npm-pack path below
|
|
204
|
+
// when nothing is bundled/installed yet.
|
|
205
|
+
mihomoBin = runtime.ensureFromBundle("mihomo") || runtime.binPath("mihomo");
|
|
192
206
|
if (!mihomoBin) {
|
|
193
207
|
const prevPath = process.env.PATH;
|
|
194
208
|
process.env.PATH = `${nodeBinDir}:${BREW_DIRS.join(":")}:${process.env.PATH || ""}`; // 让 runtime 的 execFile("npm") 命中 node24
|
|
@@ -203,7 +217,9 @@ async function ensureEnv({ emit } = {}) {
|
|
|
203
217
|
} finally { process.env.PATH = prevPath; }
|
|
204
218
|
}
|
|
205
219
|
if (mihomoBin) {
|
|
206
|
-
|
|
220
|
+
// runtime.js already owns ~/.local/bin/mihomo (symlink → mihomo-<ver>); no
|
|
221
|
+
// extra linking here (mihomoBin is itself under ~/.local/bin now, so the old
|
|
222
|
+
// linkBinsToDefaultPath call would have symlinked it to itself).
|
|
207
223
|
e({ phase: "deps", status: "done", message: t("sidecar.mihomoReady", { bin: mihomoBin }) });
|
|
208
224
|
}
|
|
209
225
|
} catch (err) {
|
|
@@ -235,10 +251,17 @@ function probeExisting(port = DEFAULT_PORT, timeoutMs = 500) {
|
|
|
235
251
|
|
|
236
252
|
let child = null;
|
|
237
253
|
|
|
238
|
-
// 「局域网访问」开关持久化: 存 ~/cicy-ai/
|
|
254
|
+
// 「局域网访问」开关持久化: 存 ~/cicy-ai/db/desktop-flags.json 的 public 字段。
|
|
239
255
|
// start() 读它决定 npx cicy-code 加不加 --public。renderer 通过 sidecar:getPublic/setPublic 读写。
|
|
240
|
-
|
|
241
|
-
|
|
256
|
+
// 不再放 ~/cicy-ai/runtime;老位置只读兜底,首次写入即迁移(public/cft token 不丢)。
|
|
257
|
+
const FLAGS_FILE = path.join(os.homedir(), "cicy-ai", "db", "desktop-flags.json");
|
|
258
|
+
const LEGACY_FLAGS_FILE = path.join(os.homedir(), "cicy-ai", "runtime", "desktop-flags.json");
|
|
259
|
+
function readFlags() {
|
|
260
|
+
for (const p of [FLAGS_FILE, LEGACY_FLAGS_FILE]) {
|
|
261
|
+
try { const v = JSON.parse(fs.readFileSync(p, "utf8")); if (v && typeof v === "object") return v; } catch {}
|
|
262
|
+
}
|
|
263
|
+
return {};
|
|
264
|
+
}
|
|
242
265
|
function isPublic() { return !!readFlags().public; }
|
|
243
266
|
function setPublicFlag(on) {
|
|
244
267
|
const f = readFlags(); f.public = !!on;
|
|
@@ -30,7 +30,12 @@ pick_registry() {
|
|
|
30
30
|
fi
|
|
31
31
|
}
|
|
32
32
|
REG="$(pick_registry)"
|
|
33
|
-
|
|
33
|
+
# cicy-code's binary tree now lives under ~/.local/cicy-code/<ver>/, matching
|
|
34
|
+
# cicy-code v2.3.193+ (api/cicy-code-update.sh). CICY_CODE_STORE overrides it.
|
|
35
|
+
# versions.json deliberately STAYS under ~/cicy-ai/runtime — it's the SHARED
|
|
36
|
+
# pointer the container's mihomo store & the Go server also read; only the
|
|
37
|
+
# cicy-code binary tree moved off runtime.
|
|
38
|
+
RT="${CICY_CODE_STORE:-$HOME_DIR/.local/cicy-code}"
|
|
34
39
|
LINK="$HOME_DIR/.local/bin/cicy-code"
|
|
35
40
|
VERSIONS="$HOME_DIR/cicy-ai/runtime/versions.json"
|
|
36
41
|
SVCTL="supervisorctl -c /etc/supervisor/supervisord.conf"
|
|
@@ -37,8 +37,27 @@ function archStr() { return process.arch === "arm64" ? "arm64" : "amd64"; }
|
|
|
37
37
|
// OSS mirror of cicy-ai/cicy-mihomo releases (see mihomo/<ver>/mihomo-<os>-<arch>).
|
|
38
38
|
function assetUrl() { return process.env.CICY_MIHOMO_RELEASE_URL || `${OSS_BASE}/mihomo/${VER}/mihomo-${osStr()}-${archStr()}${EXT}`; }
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Store under ~/.local/bin like cicy-code / cicy-mihomo — never ~/cicy-ai/runtime
|
|
41
|
+
// anymore. Old runtime-store copies still RESOLVE (read-only compat) so an
|
|
42
|
+
// existing install isn't re-downloaded; ensureBinary migrates them in.
|
|
43
|
+
const VERD = VER.replace(/^v/, "");
|
|
44
|
+
const LOCAL_BIN = path.join(os.homedir(), ".local", "bin");
|
|
45
|
+
const NEW_BIN = path.join(LOCAL_BIN, `mihomo-${VERD}${EXT}`);
|
|
46
|
+
const LINK = path.join(LOCAL_BIN, `mihomo${EXT}`);
|
|
47
|
+
const LEGACY_BIN = path.join(os.homedir(), "cicy-ai", "runtime", "mihomo", VERD, "mihomo" + EXT);
|
|
48
|
+
function valid(p) { try { return fs.statSync(p).size > 1_000_000; } catch { return false; } }
|
|
49
|
+
// resolve: ~/.local/bin versioned file first, legacy runtime store as fallback.
|
|
50
|
+
function binPath() { return valid(NEW_BIN) ? NEW_BIN : valid(LEGACY_BIN) ? LEGACY_BIN : NEW_BIN; }
|
|
51
|
+
// ~/.local/bin/mihomo → versioned file. We exec via the absolute versioned path,
|
|
52
|
+
// so this symlink is only a PATH convenience — create it only when absent, never
|
|
53
|
+
// clobber one that runtime.js / cicy-mihomo already points at their version.
|
|
54
|
+
function ensureLink() {
|
|
55
|
+
try {
|
|
56
|
+
if (!valid(NEW_BIN) || fs.existsSync(LINK)) return;
|
|
57
|
+
if (IS_WIN) { fs.copyFileSync(NEW_BIN, LINK); return; }
|
|
58
|
+
fs.symlinkSync(NEW_BIN, LINK);
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
42
61
|
const HOST_CONFIG = path.join(os.homedir(), "cicy-ai", "db", "mihomo-host.yaml");
|
|
43
62
|
// Log lives under ~/logs, NOT db/ — db/ holds data + config only (config below
|
|
44
63
|
// stays in db/ as mihomo-host.yaml; this is just the runtime log).
|
|
@@ -50,7 +69,7 @@ const PID_FILE = path.join(os.homedir(), "cicy-ai", "db", "mihomo-host.pid");
|
|
|
50
69
|
const HOST_MIXED = Number(process.env.CICY_HOST_MIHOMO_MIXED || 9011);
|
|
51
70
|
const HOST_CTRL = Number(process.env.CICY_HOST_MIHOMO_CTRL || 19011);
|
|
52
71
|
|
|
53
|
-
function binPresent() {
|
|
72
|
+
function binPresent() { return valid(NEW_BIN) || valid(LEGACY_BIN); }
|
|
54
73
|
|
|
55
74
|
function download(url, dest, redirects = 0) {
|
|
56
75
|
return new Promise((resolve, reject) => {
|
|
@@ -70,15 +89,26 @@ function download(url, dest, redirects = 0) {
|
|
|
70
89
|
});
|
|
71
90
|
}
|
|
72
91
|
|
|
73
|
-
//
|
|
92
|
+
// Ensure the mihomo binary is in ~/.local/bin/mihomo-<ver> (idempotent).
|
|
93
|
+
// Already there → done. Only a legacy runtime copy → migrate it (no network).
|
|
94
|
+
// Nothing → pull from OSS.
|
|
74
95
|
async function ensureBinary({ emit } = {}) {
|
|
75
|
-
if (
|
|
76
|
-
fs.mkdirSync(
|
|
96
|
+
if (valid(NEW_BIN)) { ensureLink(); return NEW_BIN; }
|
|
97
|
+
fs.mkdirSync(LOCAL_BIN, { recursive: true });
|
|
98
|
+
if (valid(LEGACY_BIN)) {
|
|
99
|
+
// atomic: stage then rename, so an interrupted copy never leaves a partial
|
|
100
|
+
// NEW_BIN that valid() would later mistake for a good binary.
|
|
101
|
+
const stg = NEW_BIN + ".mig";
|
|
102
|
+
try { fs.copyFileSync(LEGACY_BIN, stg); if (!IS_WIN) fs.chmodSync(stg, 0o755); fs.renameSync(stg, NEW_BIN); }
|
|
103
|
+
catch { try { fs.rmSync(stg, { force: true }); } catch {} }
|
|
104
|
+
if (valid(NEW_BIN)) { ensureLink(); return NEW_BIN; }
|
|
105
|
+
}
|
|
77
106
|
emit && emit({ phase: "chrome-proxy", status: "running", message: tt("downloading") });
|
|
78
|
-
await download(assetUrl(),
|
|
79
|
-
if (!IS_WIN) { try { fs.chmodSync(
|
|
80
|
-
if (!
|
|
81
|
-
|
|
107
|
+
await download(assetUrl(), NEW_BIN);
|
|
108
|
+
if (!IS_WIN) { try { fs.chmodSync(NEW_BIN, 0o755); } catch {} }
|
|
109
|
+
if (!valid(NEW_BIN)) throw new Error(tt("verifyFailed"));
|
|
110
|
+
ensureLink();
|
|
111
|
+
return NEW_BIN;
|
|
82
112
|
}
|
|
83
113
|
|
|
84
114
|
// Build the HOST config from the container's mihomo.yaml: keep only what Chrome
|
package/src/sidecar/runtime.js
CHANGED
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
// Copyright 2026 CiCy AI
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
// Runtime Bundle
|
|
5
|
-
// runs locally (cicy-code, mihomo).
|
|
4
|
+
// Runtime Bundle v2 — versioned component store for everything cicy-desktop
|
|
5
|
+
// runs locally (cicy-code, mihomo). Everything installs under ~/.local/bin,
|
|
6
|
+
// matching cicy-code / cicy-mihomo: the store is on PATH, never in a private
|
|
7
|
+
// ~/cicy-ai/runtime tree anymore.
|
|
6
8
|
//
|
|
7
|
-
// Layout:
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// Layout (writes ONLY here):
|
|
10
|
+
// ~/.local/bin/<comp>-<ver> versioned binary (e.g. mihomo-1.10.4)
|
|
11
|
+
// ~/.local/bin/<comp> -> <comp>-<ver> symlink we swap on switch (atomic
|
|
12
|
+
// relink; win32 copies instead —
|
|
13
|
+
// symlink needs admin there)
|
|
14
|
+
// ~/cicy-ai/db/local-bin-versions.json { "<comp>": { "current", "previous" } }
|
|
12
15
|
//
|
|
13
|
-
//
|
|
16
|
+
// Resolution order (READS, with backward compat):
|
|
17
|
+
// ~/.local/bin/<comp>-<current> → ~/.local/bin/<comp> symlink →
|
|
18
|
+
// legacy ~/cicy-ai/runtime/<comp>/<ver>/<bin> (old installs still resolve
|
|
19
|
+
// until their next upgrade migrates them into ~/.local/bin).
|
|
20
|
+
//
|
|
21
|
+
// Sourcing order for the binary itself:
|
|
14
22
|
// 1. first run: copy out of cicy-desktop's own node_modules — the platform
|
|
15
23
|
// subpackages are optionalDependencies, so `npm i -g cicy-desktop`
|
|
16
|
-
// already delivered the right binaries. First start = ZERO network
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// The `current` pointer lives in versions.json (NOT a symlink — Windows
|
|
23
|
-
// junction/symlink permissions are a minefield; a JSON pointer is identical
|
|
24
|
-
// on every platform).
|
|
24
|
+
// already delivered the right binaries. First start = ZERO network.
|
|
25
|
+
// 2. migrate: if only a legacy ~/cicy-ai/runtime binary exists, copy it into
|
|
26
|
+
// ~/.local/bin (no network) so the new layout takes over.
|
|
27
|
+
// 3. upgrades: `npm pack <pkg>@<ver>` (npmmirror default) → extract → install
|
|
28
|
+
// into ~/.local/bin → caller verifies health → switchCurrent(). The
|
|
29
|
+
// previous versioned file stays on disk for instant rollback.
|
|
25
30
|
const { execFile } = require("child_process");
|
|
26
31
|
const fs = require("fs");
|
|
27
32
|
const os = require("os");
|
|
28
33
|
const path = require("path");
|
|
29
34
|
|
|
30
|
-
const
|
|
31
|
-
const VERSIONS_JSON = path.join(
|
|
35
|
+
const LOCAL_BIN = path.join(os.homedir(), ".local", "bin");
|
|
36
|
+
const VERSIONS_JSON = path.join(os.homedir(), "cicy-ai", "db", "local-bin-versions.json");
|
|
37
|
+
// read-only backward-compat fallbacks (never written anymore)
|
|
38
|
+
const LEGACY_DIR = path.join(os.homedir(), "cicy-ai", "runtime");
|
|
39
|
+
const LEGACY_VERSIONS = path.join(LEGACY_DIR, "versions.json");
|
|
32
40
|
const REGISTRY = process.env.CICY_NPM_REGISTRY || "https://registry.npmmirror.com";
|
|
33
41
|
const IS_WIN = process.platform === "win32";
|
|
42
|
+
const EXT = IS_WIN ? ".exe" : "";
|
|
34
43
|
|
|
35
44
|
// npm subpackage platform suffix. NOTE: Windows is "windows" not "win32" —
|
|
36
45
|
// npm's spam filter 403s NEW package names containing 'win32', so EVERY cicy
|
|
@@ -48,21 +57,41 @@ const COMPONENTS = {
|
|
|
48
57
|
"cicy-code": {
|
|
49
58
|
kind: "bin",
|
|
50
59
|
pkg: () => `cicy-code-${plat()}`,
|
|
51
|
-
bin: () =>
|
|
60
|
+
bin: () => `cicy-code${EXT}`,
|
|
52
61
|
},
|
|
53
62
|
"mihomo": {
|
|
54
63
|
kind: "bin",
|
|
55
64
|
pkg: () => `cicy-mihomo-${plat()}`,
|
|
56
|
-
bin: () =>
|
|
65
|
+
bin: () => `mihomo${EXT}`,
|
|
57
66
|
},
|
|
58
67
|
};
|
|
59
68
|
|
|
69
|
+
// ~/.local/bin/<comp>-<ver>[.exe] — the versioned binary we own.
|
|
70
|
+
function binFile(comp, ver) {
|
|
71
|
+
return path.join(LOCAL_BIN, `${comp}-${ver}${EXT}`);
|
|
72
|
+
}
|
|
73
|
+
// ~/.local/bin/<comp>[.exe] — the PATH entry (symlink, or copy on win32).
|
|
74
|
+
function linkPath(comp) {
|
|
75
|
+
return path.join(LOCAL_BIN, `${comp}${EXT}`);
|
|
76
|
+
}
|
|
77
|
+
// legacy ~/cicy-ai/runtime/<comp>/<ver>/<bin> — read-only compat.
|
|
78
|
+
function legacyBinPath(comp, ver) {
|
|
79
|
+
const c = COMPONENTS[comp];
|
|
80
|
+
return c ? path.join(LEGACY_DIR, comp, ver, c.bin()) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
function readVersions() {
|
|
61
|
-
|
|
84
|
+
for (const p of [VERSIONS_JSON, LEGACY_VERSIONS]) {
|
|
85
|
+
try {
|
|
86
|
+
const v = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
87
|
+
if (v && Object.keys(v).length) return v;
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
return {};
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
function writeVersions(updater) {
|
|
65
|
-
fs.mkdirSync(
|
|
94
|
+
fs.mkdirSync(path.dirname(VERSIONS_JSON), { recursive: true });
|
|
66
95
|
const next = updater(readVersions()) || {};
|
|
67
96
|
const tmp = VERSIONS_JSON + ".tmp";
|
|
68
97
|
fs.writeFileSync(tmp, JSON.stringify(next, null, 2));
|
|
@@ -74,51 +103,87 @@ function currentVersion(comp) {
|
|
|
74
103
|
return readVersions()[comp]?.current || null;
|
|
75
104
|
}
|
|
76
105
|
|
|
77
|
-
function versionDir(comp, ver) {
|
|
78
|
-
return path.join(RUNTIME_DIR, comp, ver);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
106
|
// Absolute path of the component's current executable. null when not installed.
|
|
107
|
+
// ~/.local/bin versioned file → symlink → legacy runtime store (compat).
|
|
82
108
|
function binPath(comp) {
|
|
83
109
|
const c = COMPONENTS[comp];
|
|
110
|
+
if (!c) return null;
|
|
84
111
|
const ver = currentVersion(comp);
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
if (ver) {
|
|
113
|
+
const vf = binFile(comp, ver);
|
|
114
|
+
if (fs.existsSync(vf)) return vf;
|
|
115
|
+
}
|
|
116
|
+
const lp = linkPath(comp);
|
|
117
|
+
if (fs.existsSync(lp)) return lp;
|
|
118
|
+
if (ver) {
|
|
119
|
+
const legacy = legacyBinPath(comp, ver);
|
|
120
|
+
if (legacy && fs.existsSync(legacy)) return legacy; // old install, pre-migration
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
88
123
|
}
|
|
89
124
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
125
|
+
// Copy a single executable file into ~/.local/bin/<comp>-<ver>. Atomic-ish
|
|
126
|
+
// (stage under a temp name, then rename). Idempotent.
|
|
127
|
+
function installFromFile(comp, ver, srcFile) {
|
|
128
|
+
const dst = binFile(comp, ver);
|
|
129
|
+
if (fs.existsSync(dst)) return dst;
|
|
130
|
+
if (!fs.existsSync(srcFile)) throw new Error(`${comp}: source binary missing (${srcFile})`);
|
|
131
|
+
fs.mkdirSync(LOCAL_BIN, { recursive: true });
|
|
97
132
|
const staging = dst + ".staging";
|
|
98
|
-
fs.rmSync(staging, {
|
|
99
|
-
fs.
|
|
100
|
-
|
|
101
|
-
if (!fs.existsSync(src)) { fs.rmSync(staging, { recursive: true, force: true }); throw new Error(`${comp}: ${c.bin()} missing in package`); }
|
|
102
|
-
fs.copyFileSync(src, path.join(staging, c.bin()));
|
|
103
|
-
if (!IS_WIN) fs.chmodSync(path.join(staging, c.bin()), 0o755);
|
|
133
|
+
try { fs.rmSync(staging, { force: true }); } catch {}
|
|
134
|
+
fs.copyFileSync(srcFile, staging);
|
|
135
|
+
if (!IS_WIN) fs.chmodSync(staging, 0o755);
|
|
104
136
|
fs.renameSync(staging, dst);
|
|
105
137
|
return dst;
|
|
106
138
|
}
|
|
107
139
|
|
|
140
|
+
// Install a payload directory (the npm package dir) as <comp>@<ver>.
|
|
141
|
+
function installPayload(comp, ver, payloadDir) {
|
|
142
|
+
const c = COMPONENTS[comp];
|
|
143
|
+
return installFromFile(comp, ver, path.join(payloadDir, c.bin()));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Point ~/.local/bin/<comp> at <comp>-<ver>. Symlink on posix (atomic
|
|
147
|
+
// tmp+rename), copy on win32 (symlink there needs admin — same call the
|
|
148
|
+
// cicy-mihomo/cicy-code installers make).
|
|
149
|
+
function relink(comp, ver) {
|
|
150
|
+
const target = binFile(comp, ver);
|
|
151
|
+
if (!fs.existsSync(target)) return;
|
|
152
|
+
const link = linkPath(comp);
|
|
153
|
+
fs.mkdirSync(LOCAL_BIN, { recursive: true });
|
|
154
|
+
if (IS_WIN) {
|
|
155
|
+
try { fs.rmSync(link, { force: true }); } catch {}
|
|
156
|
+
try { fs.copyFileSync(target, link); } catch {}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const tmp = `${link}.tmp-${process.pid}`;
|
|
160
|
+
try {
|
|
161
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
162
|
+
fs.symlinkSync(target, tmp);
|
|
163
|
+
fs.renameSync(tmp, link); // atomically replaces the old symlink
|
|
164
|
+
} catch {
|
|
165
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
108
169
|
function switchCurrent(comp, ver) {
|
|
109
170
|
writeVersions((v) => { v[comp] = { ...(v[comp] || {}), current: ver, switched_at: new Date().toISOString() }; return v; });
|
|
171
|
+
relink(comp, ver);
|
|
110
172
|
}
|
|
111
173
|
|
|
112
|
-
// Keep current + previous; GC everything older.
|
|
174
|
+
// Keep current + previous versioned files; GC everything older in ~/.local/bin.
|
|
113
175
|
function gc(comp) {
|
|
114
176
|
const cur = currentVersion(comp);
|
|
115
|
-
const dir = path.join(RUNTIME_DIR, comp);
|
|
116
|
-
let entries = [];
|
|
117
|
-
try { entries = fs.readdirSync(dir).filter((d) => !d.endsWith(".staging")); } catch { return; }
|
|
118
177
|
const prev = readVersions()[comp]?.previous;
|
|
178
|
+
const prefix = `${comp}-`;
|
|
179
|
+
let entries = [];
|
|
180
|
+
try { entries = fs.readdirSync(LOCAL_BIN); } catch { return; }
|
|
119
181
|
for (const e of entries) {
|
|
120
|
-
if (e
|
|
121
|
-
|
|
182
|
+
if (!e.startsWith(prefix)) continue; // skips the bare "<comp>" symlink too
|
|
183
|
+
if (e.endsWith(".staging") || e.includes(".tmp-")) { try { fs.rmSync(path.join(LOCAL_BIN, e), { force: true }); } catch {} continue; }
|
|
184
|
+
const ver = e.slice(prefix.length).replace(/\.exe$/, "");
|
|
185
|
+
if (ver !== cur && ver !== prev) {
|
|
186
|
+
try { fs.rmSync(path.join(LOCAL_BIN, e), { force: true }); } catch {}
|
|
122
187
|
}
|
|
123
188
|
}
|
|
124
189
|
}
|
|
@@ -139,18 +204,33 @@ function bundledPkgDir(comp) {
|
|
|
139
204
|
return null;
|
|
140
205
|
}
|
|
141
206
|
|
|
142
|
-
// First-run seeding: make sure
|
|
143
|
-
//
|
|
207
|
+
// First-run seeding / migration: make sure the current `comp` binary lives in
|
|
208
|
+
// ~/.local/bin and its symlink is current. Never touches the network.
|
|
209
|
+
// • already in ~/.local/bin → just fix the symlink.
|
|
210
|
+
// • only a legacy ~/cicy-ai/runtime copy → migrate it into ~/.local/bin.
|
|
211
|
+
// • nothing → copy from the bundled subpackage.
|
|
144
212
|
function ensureFromBundle(comp) {
|
|
145
213
|
const c = COMPONENTS[comp];
|
|
146
214
|
if (!c) return null;
|
|
147
|
-
const
|
|
148
|
-
|
|
215
|
+
const cur = currentVersion(comp);
|
|
216
|
+
|
|
217
|
+
if (cur && fs.existsSync(binFile(comp, cur))) { relink(comp, cur); return binFile(comp, cur); }
|
|
218
|
+
|
|
219
|
+
// migrate a legacy runtime-store install into ~/.local/bin (no network)
|
|
220
|
+
if (cur) {
|
|
221
|
+
const legacy = legacyBinPath(comp, cur);
|
|
222
|
+
if (legacy && fs.existsSync(legacy)) {
|
|
223
|
+
try { installFromFile(comp, cur, legacy); switchCurrent(comp, cur); return binPath(comp); } catch {}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// seed from the bundled optionalDependency subpackage
|
|
149
228
|
const pkgDir = bundledPkgDir(comp);
|
|
150
|
-
if (!pkgDir) return
|
|
229
|
+
if (!pkgDir) return binPath(comp); // dev tree / not bundled — may still resolve via legacy or symlink
|
|
151
230
|
const ver = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8")).version;
|
|
152
231
|
installPayload(comp, ver, pkgDir);
|
|
153
|
-
if (!currentVersion(comp)) switchCurrent(comp, ver);
|
|
232
|
+
if (!currentVersion(comp) || !fs.existsSync(binFile(comp, currentVersion(comp)))) switchCurrent(comp, ver);
|
|
233
|
+
else relink(comp, currentVersion(comp));
|
|
154
234
|
return binPath(comp);
|
|
155
235
|
}
|
|
156
236
|
|
|
@@ -171,14 +251,14 @@ async function checkUpdate(comp) {
|
|
|
171
251
|
}
|
|
172
252
|
|
|
173
253
|
// Download <pkg>@<ver> via `npm pack` (pacote verifies sha512 integrity),
|
|
174
|
-
// extract, install
|
|
175
|
-
//
|
|
254
|
+
// extract, install into ~/.local/bin. Does NOT switch `current` — the caller
|
|
255
|
+
// health-checks first, then calls switchCurrent()/rollback.
|
|
176
256
|
async function fetchVersion(comp, ver, { emit } = {}) {
|
|
177
257
|
const c = COMPONENTS[comp];
|
|
178
258
|
const e = emit || (() => {});
|
|
179
|
-
if (fs.existsSync(
|
|
259
|
+
if (fs.existsSync(binFile(comp, ver))) {
|
|
180
260
|
e({ phase: "download", status: "skip", message: `${comp} ${ver} 已在本地,跳过下载` });
|
|
181
|
-
return
|
|
261
|
+
return binFile(comp, ver);
|
|
182
262
|
}
|
|
183
263
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `cicy-rt-${comp}-`));
|
|
184
264
|
try {
|
|
@@ -189,9 +269,9 @@ async function fetchVersion(comp, ver, { emit } = {}) {
|
|
|
189
269
|
execFile("tar", ["-xzf", tgz, "-C", tmp], { windowsHide: true, timeout: 120000 },
|
|
190
270
|
(err) => (err ? reject(err) : resolve()));
|
|
191
271
|
});
|
|
192
|
-
const
|
|
272
|
+
const file = installPayload(comp, ver, path.join(tmp, "package"));
|
|
193
273
|
e({ phase: "download", status: "done", message: `${comp} ${ver} 就绪` });
|
|
194
|
-
return
|
|
274
|
+
return file;
|
|
195
275
|
} finally {
|
|
196
276
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
197
277
|
}
|
|
@@ -220,7 +300,7 @@ async function upgrade(comp, { emit, stop, start, verify } = {}) {
|
|
|
220
300
|
} catch (err) {
|
|
221
301
|
// rollback: pointer back, restart old version
|
|
222
302
|
e({ phase: "swap", status: "error", message: `新版本异常(${err.message}),回滚到 ${from}` });
|
|
223
|
-
switchCurrent(comp, from);
|
|
303
|
+
if (from) switchCurrent(comp, from);
|
|
224
304
|
if (start) { try { await start(); } catch {} }
|
|
225
305
|
return { ok: false, from, to: latest, rolledBack: true, error: err.message };
|
|
226
306
|
}
|
|
@@ -230,7 +310,7 @@ async function upgrade(comp, { emit, stop, start, verify } = {}) {
|
|
|
230
310
|
}
|
|
231
311
|
|
|
232
312
|
module.exports = {
|
|
233
|
-
|
|
234
|
-
binPath, currentVersion, ensureFromBundle, fetchVersion, switchCurrent,
|
|
313
|
+
LOCAL_BIN, COMPONENTS,
|
|
314
|
+
binPath, binFile, linkPath, currentVersion, ensureFromBundle, fetchVersion, switchCurrent,
|
|
235
315
|
checkUpdate, upgrade, gc, readVersions,
|
|
236
316
|
};
|