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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.60",
3
+ "version": "2.1.61",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -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
- return await docker.bootstrap({
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
  }
@@ -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 — start() already reuses/replaces by name.
289
- emit({ phase: "container", status: "running", message: "启动 cicy-code 容器…" });
290
- const child = await start({ port });
291
- if (!child) {
292
- emit({ phase: "container", status: "error", message: "容器启动失败" });
293
- return { ok: false, reason: "container_start_failed" };
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