cicy-desktop 2.1.60 → 2.1.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/backends/sidecar-ipc.js +10 -1
- package/src/sidecar/docker.js +65 -7
package/package.json
CHANGED
|
@@ -45,10 +45,19 @@ function register({ sidecarLogPath } = {}) {
|
|
|
45
45
|
ipcMain.handle("docker:bootstrap", async (e) => {
|
|
46
46
|
if (process.platform !== "win32") return { ok: false, error: "docker bootstrap is Windows-only" };
|
|
47
47
|
try {
|
|
48
|
-
|
|
48
|
+
const result = await docker.bootstrap({
|
|
49
49
|
port: PORT,
|
|
50
50
|
onProgress: (ev) => { try { e.sender.send("docker:bootstrap-progress", ev); } catch {} },
|
|
51
51
|
});
|
|
52
|
+
// Healthy local stack → make sure it shows up as a team ("本地团队就加
|
|
53
|
+
// 上去了"). addTeam dedups by host:port, so re-runs are no-ops.
|
|
54
|
+
if (result && result.ok) {
|
|
55
|
+
try {
|
|
56
|
+
const lt = require("./local-teams");
|
|
57
|
+
await lt.addTeam({ base_url: `http://127.0.0.1:${PORT}`, name: "本地团队" });
|
|
58
|
+
} catch { /* best-effort — the stack itself is up */ }
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
52
61
|
} catch (err) {
|
|
53
62
|
return { ok: false, error: err.message };
|
|
54
63
|
}
|
package/src/sidecar/docker.js
CHANGED
|
@@ -42,6 +42,28 @@ async function dockerOk() {
|
|
|
42
42
|
catch { return false; }
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Docker Desktop installed on disk? (daemon may still be stopped — dockerOk()
|
|
46
|
+
// only answers "is the daemon up"). Lets bootstrap start the app instead of
|
|
47
|
+
// re-downloading the 500MB installer when it's merely not running.
|
|
48
|
+
function dockerDesktopExe() {
|
|
49
|
+
const candidates = [
|
|
50
|
+
path.join(process.env["ProgramFiles"] || "C:\\Program Files", "Docker", "Docker", "Docker Desktop.exe"),
|
|
51
|
+
path.join(process.env["LOCALAPPDATA"] || "", "Docker", "Docker Desktop.exe"),
|
|
52
|
+
];
|
|
53
|
+
for (const p of candidates) { try { if (p && fs.existsSync(p)) return p; } catch {} }
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function startDockerDesktop() {
|
|
58
|
+
const exe = dockerDesktopExe();
|
|
59
|
+
if (!exe) return false;
|
|
60
|
+
try {
|
|
61
|
+
const child = spawn(exe, [], { detached: true, stdio: "ignore", windowsHide: false });
|
|
62
|
+
child.unref();
|
|
63
|
+
return true;
|
|
64
|
+
} catch { return false; }
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
async function imagePresent() {
|
|
46
68
|
try { await run(["image", "inspect", IMAGE], { timeout: 8000 }); return true; }
|
|
47
69
|
catch { return false; }
|
|
@@ -143,12 +165,15 @@ async function ensureDownloaded(url, dest, mirror, { emit, phase, label } = {})
|
|
|
143
165
|
return dest;
|
|
144
166
|
}
|
|
145
167
|
const sources = mirror ? [url, mirror] : [url];
|
|
168
|
+
let lastPct = -1; // throttle: chunks arrive dozens/s — only emit on whole-percent change
|
|
146
169
|
return withRetry(async (attempt) => {
|
|
147
170
|
const src = sources[Math.min(attempt - 1, sources.length - 1)];
|
|
148
171
|
await download(src, dest, {
|
|
149
172
|
resume: true,
|
|
150
173
|
onProgress: ({ received, total }) => {
|
|
151
174
|
const pct = total ? Math.round((received / total) * 100) : 0;
|
|
175
|
+
if (pct === lastPct) return;
|
|
176
|
+
lastPct = pct;
|
|
152
177
|
emit && emit({ phase, status: "running", message: label, progress: pct, received, total });
|
|
153
178
|
},
|
|
154
179
|
});
|
|
@@ -182,7 +207,15 @@ async function loadImage({ emit } = {}) {
|
|
|
182
207
|
await ensureDownloaded(R2_TARBALL, tmp, null, { emit, phase: "image", label: "下载镜像" });
|
|
183
208
|
emit && emit({ phase: "image", status: "running", message: "docker load…", progress: 100 });
|
|
184
209
|
console.log(`[docker-sidecar] docker load…`);
|
|
185
|
-
await run(["load", "-i", tmp], { timeout: 300000 });
|
|
210
|
+
const { stdout } = await run(["load", "-i", tmp], { timeout: 300000 });
|
|
211
|
+
// The tarball's embedded tag may be a pinned version (e.g. :2.1.6) while we
|
|
212
|
+
// run IMAGE (:latest). Re-tag whatever was loaded so imagePresent()/start()
|
|
213
|
+
// match — otherwise every start() re-downloads the tarball forever.
|
|
214
|
+
const m = String(stdout).match(/Loaded image:\s*(\S+)/i);
|
|
215
|
+
if (m && m[1] !== IMAGE) {
|
|
216
|
+
try { await run(["tag", m[1], IMAGE]); console.log(`[docker-sidecar] tagged ${m[1]} -> ${IMAGE}`); }
|
|
217
|
+
catch (e) { console.warn(`[docker-sidecar] re-tag failed: ${e.message}`); }
|
|
218
|
+
}
|
|
186
219
|
// Only delete AFTER a successful load — a failed load keeps the tarball so the
|
|
187
220
|
// next attempt skips the re-download. (imagePresent() gates re-entry anyway.)
|
|
188
221
|
try { fs.unlinkSync(tmp); } catch {}
|
|
@@ -197,6 +230,13 @@ async function checkStatus() {
|
|
|
197
230
|
// id } or null when Docker isn't ready (homepage guides the user to install
|
|
198
231
|
// Docker Desktop).
|
|
199
232
|
async function start({ port = 8008 } = {}) {
|
|
233
|
+
// Something already serves a healthy cicy-code on :port (a legacy-named
|
|
234
|
+
// container auto-revived by `--restart unless-stopped`, a manual run…).
|
|
235
|
+
// Adopt it — `docker run` would just lose the port-bind fight.
|
|
236
|
+
if (await probeHealth(port)) {
|
|
237
|
+
console.log(`[docker-sidecar] :${port} already healthy — adopting existing instance`);
|
|
238
|
+
return { docker: true, container: CONTAINER, adopted: true };
|
|
239
|
+
}
|
|
200
240
|
if (!(await dockerOk())) {
|
|
201
241
|
console.warn("[docker-sidecar] Docker not available — homepage will guide install");
|
|
202
242
|
return null;
|
|
@@ -261,6 +301,17 @@ async function bootstrap({ onProgress, port = 8008 } = {}) {
|
|
|
261
301
|
// 1) Docker present?
|
|
262
302
|
if (await dockerOk()) {
|
|
263
303
|
emit({ phase: "install-docker", status: "skip", message: "Docker 已安装,跳过" });
|
|
304
|
+
} else if (dockerDesktopExe()) {
|
|
305
|
+
// Installed but the daemon is down — just launch Docker Desktop, never
|
|
306
|
+
// re-download/re-run the installer ("步骤走过的不要再走").
|
|
307
|
+
emit({ phase: "install-docker", status: "running", message: "Docker 已安装,正在启动 Docker Desktop…" });
|
|
308
|
+
startDockerDesktop();
|
|
309
|
+
const up = await waitUntil(dockerOk, { totalMs: 300000, everyMs: 5000 });
|
|
310
|
+
if (!up) {
|
|
311
|
+
emit({ phase: "install-docker", status: "error", message: "Docker Desktop 启动超时——手动打开它等图标变绿,再点「重试」" });
|
|
312
|
+
return { ok: false, reason: "docker_not_ready" };
|
|
313
|
+
}
|
|
314
|
+
emit({ phase: "install-docker", status: "done", message: "Docker 就绪" });
|
|
264
315
|
} else {
|
|
265
316
|
await installDocker({ emit });
|
|
266
317
|
emit({ phase: "install-docker", status: "running", message: "等待 Docker 启动(如需授权/重启,完成后会自动继续)…" });
|
|
@@ -285,12 +336,19 @@ async function bootstrap({ onProgress, port = 8008 } = {}) {
|
|
|
285
336
|
}
|
|
286
337
|
}
|
|
287
338
|
|
|
288
|
-
// 3) Container —
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
339
|
+
// 3) Container — skip when :port is already healthy (idempotent re-entry);
|
|
340
|
+
// start() additionally adopts an existing instance instead of port-fighting.
|
|
341
|
+
if (await probeHealth(port)) {
|
|
342
|
+
emit({ phase: "container", status: "skip", message: "本地服务已在运行,跳过" });
|
|
343
|
+
} else {
|
|
344
|
+
emit({ phase: "container", status: "running", message: "启动 cicy-code 容器…" });
|
|
345
|
+
let child = null;
|
|
346
|
+
try { child = await start({ port }); }
|
|
347
|
+
catch (e) { emit({ phase: "container", status: "error", message: `容器启动失败:${e.message}` }); return { ok: false, reason: "container_start_failed" }; }
|
|
348
|
+
if (!child) {
|
|
349
|
+
emit({ phase: "container", status: "error", message: "容器启动失败" });
|
|
350
|
+
return { ok: false, reason: "container_start_failed" };
|
|
351
|
+
}
|
|
294
352
|
}
|
|
295
353
|
|
|
296
354
|
// 4) Health
|