cicy-desktop 2.1.71 → 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/.cicy-code-ref +1 -1
- package/.github/workflows/linux-app-release.yml +3 -0
- package/.github/workflows/mac-app-release.yml +3 -0
- package/.github/workflows/npm-publish.yml +32 -0
- package/.github/workflows/windows-exe-release.yml +3 -0
- package/package.json +2 -3
- package/scripts/sync-runtime-deps.cjs +54 -0
- package/src/sidecar/cicy-code.js +21 -7
- package/src/sidecar/localbin.js +198 -63
- package/workers/render/src/App.css +4 -0
package/.cicy-code-ref
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v2.
|
|
1
|
+
v2.2.5
|
|
@@ -53,6 +53,9 @@ jobs:
|
|
|
53
53
|
SKIP_TTYD_ASSET=1 SKIP_SKILLS_EMBED=1 bash build.sh all
|
|
54
54
|
ls -lh dist/
|
|
55
55
|
|
|
56
|
+
- name: Sync runtime deps to latest (cicy-code + cicy-mihomo; drop msys2)
|
|
57
|
+
run: node scripts/sync-runtime-deps.cjs
|
|
58
|
+
|
|
56
59
|
- name: Install project dependencies
|
|
57
60
|
run: npm install --no-audit
|
|
58
61
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
# Tag push (vX.Y.Z) → sync optionalDependencies to the LATEST per-platform
|
|
4
|
+
# cicy-code + cicy-mihomo, then publish cicy-desktop to npm. This is what
|
|
5
|
+
# `npx cicy-desktop` / `npm i -g cicy-desktop` users get — the app-installer
|
|
6
|
+
# workflows only build dmg/exe/AppImage, they do NOT publish npm.
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags:
|
|
10
|
+
- 'v*'
|
|
11
|
+
workflow_dispatch: {}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Setup Node.js
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: '20'
|
|
24
|
+
registry-url: 'https://registry.npmjs.org'
|
|
25
|
+
|
|
26
|
+
- name: Sync runtime deps to latest (cicy-code + cicy-mihomo; drop msys2)
|
|
27
|
+
run: node scripts/sync-runtime-deps.cjs
|
|
28
|
+
|
|
29
|
+
- name: Publish cicy-desktop to npm
|
|
30
|
+
run: npm publish --access public
|
|
31
|
+
env:
|
|
32
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cicy-desktop",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.73",
|
|
4
4
|
"description": "CiCy - AI-powered operating system browser",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -143,8 +143,7 @@
|
|
|
143
143
|
"cicy-mihomo-linux-x64": "1.10.4",
|
|
144
144
|
"cicy-mihomo-linux-arm64": "1.10.4",
|
|
145
145
|
"cicy-mihomo-windows-x64": "1.10.4",
|
|
146
|
-
"cicy-mihomo-windows-arm64": "1.10.4"
|
|
147
|
-
"cicy-msys2-windows-x64": "1.0.0"
|
|
146
|
+
"cicy-mihomo-windows-arm64": "1.10.4"
|
|
148
147
|
},
|
|
149
148
|
"devDependencies": {
|
|
150
149
|
"@babel/core": "^7.29.0",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Sync optionalDependencies to the LATEST published per-platform cicy-code and
|
|
3
|
+
// cicy-mihomo subpackages, so every cicy-desktop build/publish bundles the
|
|
4
|
+
// newest binaries. Run in CI before install/build (and before `npm publish`).
|
|
5
|
+
//
|
|
6
|
+
// 主人指令 (2026-06-08): Windows no longer ships msys2/tmux — the win sidecar
|
|
7
|
+
// runs the single headless 团队助手 (--helper), so cicy-msys2-* is DROPPED here.
|
|
8
|
+
//
|
|
9
|
+
// Usage: node scripts/sync-runtime-deps.cjs (writes package.json in place)
|
|
10
|
+
|
|
11
|
+
const { execFileSync } = require("child_process");
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
|
|
15
|
+
const REGISTRY = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
|
|
16
|
+
const PLATFORMS = ["darwin-x64", "darwin-arm64", "linux-x64", "linux-arm64", "windows-x64", "windows-arm64"];
|
|
17
|
+
const COMPONENTS = ["cicy-code", "cicy-mihomo"]; // NOT cicy-msys2 — win drops it
|
|
18
|
+
|
|
19
|
+
function latest(pkg) {
|
|
20
|
+
try {
|
|
21
|
+
return execFileSync("npm", ["view", pkg, "version", `--registry=${REGISTRY}`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null; // not published for this platform (e.g. cicy-code has no windows-arm64)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
29
|
+
|
|
30
|
+
const resolved = {};
|
|
31
|
+
for (const comp of COMPONENTS) {
|
|
32
|
+
for (const plat of PLATFORMS) {
|
|
33
|
+
const name = `${comp}-${plat}`;
|
|
34
|
+
const v = latest(name);
|
|
35
|
+
if (v) resolved[name] = v;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (Object.keys(resolved).length === 0) {
|
|
39
|
+
console.error("[sync-runtime-deps] resolved nothing — registry unreachable? aborting (package.json untouched)");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Keep any non-runtime optionalDeps; replace cicy-code-*/cicy-mihomo-*; drop cicy-msys2-*.
|
|
44
|
+
const kept = Object.fromEntries(
|
|
45
|
+
Object.entries(pkg.optionalDependencies || {}).filter(
|
|
46
|
+
([k]) => !k.startsWith("cicy-code-") && !k.startsWith("cicy-mihomo-") && !k.startsWith("cicy-msys2")
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
pkg.optionalDependencies = { ...kept, ...resolved };
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
52
|
+
console.log("[sync-runtime-deps] optionalDependencies ->");
|
|
53
|
+
for (const [k, v] of Object.entries(resolved)) console.log(` ${k}@${v}`);
|
|
54
|
+
console.log("[sync-runtime-deps] dropped cicy-msys2-* (Windows no longer bundles msys2/tmux)");
|
package/src/sidecar/cicy-code.js
CHANGED
|
@@ -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 });
|
|
@@ -128,13 +141,8 @@ async function start({ logPath, port = DEFAULT_PORT, force = false, version = nu
|
|
|
128
141
|
};
|
|
129
142
|
const args = [];
|
|
130
143
|
if (process.platform === "win32") {
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
const runtime = require("./runtime");
|
|
134
|
-
const msys = runtime.binPath("msys2") || runtime.ensureFromBundle("msys2");
|
|
135
|
-
if (msys) env.CICY_MSYS_ROOT = msys;
|
|
136
|
-
} catch {}
|
|
137
|
-
// --helper=1: boot as the single headless cicy 团队助手 on w-1001 (开机即团队助手).
|
|
144
|
+
// Windows runs the single headless 团队助手 (--helper=1) on w-1001 — no tmux
|
|
145
|
+
// panes, so msys2/tmux are NOT bundled or referenced anymore (主人指令 2026-06-08).
|
|
138
146
|
args.push("--helper=1");
|
|
139
147
|
}
|
|
140
148
|
child = spawn(exe, args, { stdio, detached: false, windowsHide: true, env });
|
|
@@ -253,8 +261,14 @@ async function update({ logPath, port = DEFAULT_PORT, emit } = {}) {
|
|
|
253
261
|
const localbin = require("./localbin");
|
|
254
262
|
try {
|
|
255
263
|
e({ phase: "download", status: "running", message: "检查最新版本…" });
|
|
264
|
+
const cur = localbin.currentVersion();
|
|
256
265
|
const latest = await localbin.latestVersion();
|
|
257
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
|
+
}
|
|
258
272
|
await localbin.fetchToLocalBin(latest, { emit }); // download → ~/.local/bin → re-link
|
|
259
273
|
e({ phase: "swap", status: "running", message: `切换到 ${latest},启动…` });
|
|
260
274
|
await stop({ port });
|
package/src/sidecar/localbin.js
CHANGED
|
@@ -1,35 +1,120 @@
|
|
|
1
|
-
// ~/.local/bin install model for the cicy-
|
|
1
|
+
// ~/.local/bin install model for the binaries cicy-desktop OWNS.
|
|
2
2
|
//
|
|
3
|
-
// 主人指令 (2026-06): cicy-desktop OWNS
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// ~/.local/bin/cicy-code
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
|
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
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
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",
|
|
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
|
|
47
|
-
//
|
|
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(
|
|
51
|
-
if (IS_WIN)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 =
|
|
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:
|
|
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",
|
|
73
|
-
path.join(process.resourcesPath || "", "runtime-pkgs",
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
const src = path.join(dir,
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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(
|
|
98
|
-
const
|
|
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: `下载
|
|
101
|
-
const out = await npmExec(["pack", `${
|
|
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",
|
|
106
|
-
e({ phase: "download", status: "done", message:
|
|
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
|
|
114
|
-
function currentLink() {
|
|
115
|
-
return fs.existsSync(
|
|
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
|
|
119
|
-
//
|
|
120
|
-
// -
|
|
121
|
-
// -
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
130
|
-
return
|
|
259
|
+
const res = await fetchToLocalBin(latest, { emit, name });
|
|
260
|
+
return { ...res, updated: true };
|
|
131
261
|
}
|
|
132
262
|
|
|
133
|
-
module.exports = {
|
|
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;
|