cicy-desktop 2.1.30 → 2.1.32

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>CiCy Desktop</title>
7
- <script type="module" crossorigin src="./assets/index-B8FrtpTX.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-CzuQdFw8.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-CNVsvsZX.css">
9
9
  </head>
10
10
  <body>
@@ -129,9 +129,8 @@ function register(opts = {}) {
129
129
  });
130
130
  });
131
131
 
132
- if (process.platform === "win32") {
133
- try { await require("../sidecar/wsl").stop(); } catch {}
134
- }
132
+ // Windows: the daemon runs in Docker; sidecar.stop() removes the
133
+ // container. (Was wsl.stop() WSL path retired.)
135
134
  await killByPort();
136
135
  try { await sidecar.stop({ timeoutMs: 500 }); } catch {}
137
136
 
@@ -1,71 +1,39 @@
1
- // IPC handlers for the cicy-code sidecar installer.
2
- // Decoupled from backends/ipc.js so the install flow has its own clear surface.
1
+ // IPC handlers for the cicy-code sidecar.
3
2
  //
4
- // Channels:
5
- // sidecar:status → { userInstalled, userVersion, binaryPath, installing, lastProgress }
6
- // sidecar:check-latest → { ok, latest, installedVersion, network, sizeBytes, releaseUrl, error? }
7
- // sidecar:install final progress event { phase, version?, ... }; emits sidecar:progress along the way
8
- // sidecar:cancel boolean
3
+ // cicy-code is no longer downloaded by an in-app installer — the sidecar runs
4
+ // it via `npx cicy-code` (mac/linux) or Docker (Windows); see
5
+ // src/sidecar/cicy-code.js. So this surface is just lifecycle + status:
6
+ // sidecar:status → { running } is something answering on :8008?
7
+ // sidecar:start { ok, ... } — start (or reuse) the daemon
9
8
  //
10
- // All renderers receive the same `sidecar:progress` broadcast so the UI can
11
- // rejoin an in-flight install after a refresh.
9
+ // (Removed: sidecar:check-latest / install / cancel / wsl-status / wsl-install,
10
+ // along with src/sidecar/installer.js and src/sidecar/wsl.js.)
12
11
 
13
- const { ipcMain, BrowserWindow } = require("electron");
14
- const log = require("electron-log");
15
- const installer = require("../sidecar/installer");
12
+ const { ipcMain } = require("electron");
16
13
  const sidecar = require("../sidecar/cicy-code");
17
14
 
15
+ const PORT = Number(process.env.CICY_CODE_PORT || 8008);
18
16
  let registered = false;
19
- let lastProgress = null;
20
-
21
- function broadcast(event) {
22
- lastProgress = event;
23
- for (const win of BrowserWindow.getAllWindows()) {
24
- try { win.webContents.send("sidecar:progress", event); } catch {}
25
- }
26
- }
27
17
 
28
18
  function register({ sidecarLogPath } = {}) {
29
19
  if (registered) return;
30
20
  registered = true;
31
21
 
32
22
  ipcMain.handle("sidecar:status", async () => {
33
- const s = installer.getStatus();
34
- const running = await installer.isRunning();
35
- // On Windows the version cache might be stale; probe WSL for truth.
36
- if (process.platform === "win32") {
37
- try {
38
- const wsl = require("../sidecar/wsl");
39
- const [wslStatus, wslInstalled, wslVer] = await Promise.all([
40
- wsl.checkStatus(),
41
- wsl.userInstalled(),
42
- wsl.userVersion(),
43
- ]);
44
- return {
45
- ...s,
46
- userInstalled: wslInstalled,
47
- userVersion: wslVer || s.userVersion,
48
- wsl: wslStatus,
49
- running,
50
- lastProgress,
51
- };
52
- } catch {}
53
- }
54
- return { ...s, running, lastProgress };
23
+ const running = await sidecar.probeExisting(PORT);
24
+ return { running };
55
25
  });
56
26
 
57
- // Start the bundled cicy-code daemon without re-installing anything.
58
- // Used when the binary exists locally but the process isn't running yet
59
- // (e.g. user closed cicy-desktop, came back, daemon never auto-restarted).
27
+ // Start (or reuse) the cicy-code daemon. probeExisting inside start() reuses
28
+ // a healthy :8008; otherwise it spawns `npx cicy-code` / the Docker container.
60
29
  ipcMain.handle("sidecar:start", async () => {
61
30
  try {
62
- // Already up? Skip the spawn.
63
- if (await installer.isRunning()) return { ok: true, alreadyRunning: true };
31
+ if (await sidecar.probeExisting(PORT)) return { ok: true, alreadyRunning: true };
64
32
  const child = await sidecar.start({ logPath: sidecarLogPath, force: false });
65
- // Wait briefly for the daemon to bind :8008 so the homepage's poll
66
- // sees the "running" flip on the very next tick.
33
+ // Wait briefly for it to bind :8008 so the homepage's poll flips to
34
+ // "running" on the next tick.
67
35
  for (let i = 0; i < 20; i++) {
68
- if (await installer.isRunning()) return { ok: true, pid: child?.pid || null };
36
+ if (await sidecar.probeExisting(PORT)) return { ok: true, pid: child?.pid || null };
69
37
  await new Promise((r) => setTimeout(r, 250));
70
38
  }
71
39
  return { ok: true, pid: child?.pid || null, warning: "spawned but did not bind :8008 within 5s" };
@@ -73,114 +41,6 @@ function register({ sidecarLogPath } = {}) {
73
41
  return { ok: false, error: e.message };
74
42
  }
75
43
  });
76
-
77
- // Windows-only: expose WSL detection so the homepage can surface the right
78
- // setup card (install WSL → install distro → install cicy-code). On other
79
- // platforms the call is a no-op returning { supported: false }.
80
- ipcMain.handle("sidecar:wsl-status", async () => {
81
- if (process.platform !== "win32") return { supported: false };
82
- try {
83
- const wsl = require("../sidecar/wsl");
84
- const status = await wsl.checkStatus();
85
- return { supported: true, ...status };
86
- } catch (e) {
87
- return { supported: true, installed: false, error: e.message };
88
- }
89
- });
90
-
91
- // Windows-only: trigger `wsl --install`. CN-aware: when network is CN
92
- // we prefer --web-download (skips Microsoft Store, GitHub-mirror friendly).
93
- // Requires Administrator — UAC may pop. Streams progress via sidecar:progress.
94
- ipcMain.handle("sidecar:wsl-install", async () => {
95
- if (process.platform !== "win32") return { ok: false, error: "not windows" };
96
- try {
97
- const wsl = require("../sidecar/wsl");
98
- const network = await require("../sidecar/net-detect").detect();
99
- const r = await wsl.installWsl({ network, onProgress: broadcast });
100
- return r;
101
- } catch (e) {
102
- return { ok: false, error: e.message };
103
- }
104
- });
105
-
106
- ipcMain.handle("sidecar:check-latest", async () => installer.checkLatest());
107
-
108
- ipcMain.handle("sidecar:install", async () => {
109
- try {
110
- const { execFile } = require("child_process");
111
- const port = 8008;
112
-
113
- // ── Download and replace binary ───────────────────────────────────────
114
- // Do NOT unconditionally stop the daemon before downloading.
115
- // If it's owned by another OS user we can't kill it, and the binary
116
- // replacement still works on Unix (unlink old inode, rename new file).
117
- const final = await installer.install({ onProgress: broadcast });
118
-
119
- // ── Restart the daemon so the new binary takes effect ────────────────
120
- // Platform-specific because the kill primitive differs:
121
- // Windows: cicy-code lives inside WSL → wsl.stop() does pkill in distro
122
- // macOS/Linux: lsof + SIGKILL on the listening process (skip if EPERM)
123
- let restartedPid = null;
124
-
125
- if (process.platform === "win32") {
126
- try {
127
- const wsl = require("../sidecar/wsl");
128
- await wsl.stop();
129
- await new Promise(r => setTimeout(r, 500));
130
- const ch = await sidecar.start({ logPath: sidecarLogPath, force: true });
131
- if (ch?.pid) restartedPid = ch.pid;
132
- } catch (e) { log.warn(`[sidecar-ipc] win32 restart failed: ${e.message}`); }
133
-
134
- const reply = { ok: true, ...final, restartedPid };
135
- broadcast({ ...final, restartedPid });
136
- return reply;
137
- }
138
-
139
- // Find the PID *listening* on :8008 (not clients connecting to it).
140
- // Without -sTCP:LISTEN, lsof also returns processes that have open
141
- // connections TO port 8008 — including cicy-desktop's own health
142
- // probe connections — which would cause us to kill ourselves.
143
- const portPid = await new Promise(resolve => {
144
- execFile("lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"], (_, out) => {
145
- const pid = parseInt((out || "").trim().split("\n")[0], 10);
146
- resolve(isNaN(pid) ? null : pid);
147
- });
148
- });
149
-
150
- if (portPid) {
151
- const canKill = await new Promise(resolve => {
152
- try { process.kill(portPid, 9); resolve(true); }
153
- catch { resolve(false); } // EPERM: externally managed
154
- });
155
- if (canKill) {
156
- await new Promise(r => setTimeout(r, 800));
157
- try {
158
- const ch = await sidecar.start({ logPath: sidecarLogPath, force: true });
159
- if (ch?.pid) restartedPid = ch.pid;
160
- } catch (e) { log.warn(`[sidecar-ipc] restart failed: ${e.message}`); }
161
- } else {
162
- log.info(`[sidecar-ipc] :${port} is externally managed (pid ${portPid}) — binary updated, restart externally to activate`);
163
- }
164
- } else {
165
- // Nothing on :8008 — just start fresh
166
- try {
167
- const ch = await sidecar.start({ logPath: sidecarLogPath, force: true });
168
- if (ch?.pid) restartedPid = ch.pid;
169
- } catch (e) { log.warn(`[sidecar-ipc] start failed: ${e.message}`); }
170
- }
171
-
172
- const reply = { ok: true, ...final, restartedPid };
173
- broadcast({ ...final, restartedPid });
174
- return reply;
175
- } catch (e) {
176
- return { ok: false, error: e.message };
177
- }
178
- });
179
-
180
- ipcMain.handle("sidecar:cancel", () => {
181
- installer.cancel();
182
- return true;
183
- });
184
44
  }
185
45
 
186
46
  module.exports = { register };
@@ -46,15 +46,8 @@ contextBridge.exposeInMainWorld("cicy", {
46
46
  update: (id, patch) => relay("localTeams:update", { id, patch }),
47
47
  upgrade: (id) => relay("localTeams:upgrade", { id }),
48
48
  },
49
- // cicy-code daemon ("sidecar") install/updatedistinct from
50
- // localTeams.upgrade which is per-team. install() downloads the latest
51
- // binary to ~/.local/bin/cicy-code-<ver> + atomic-relinks the
52
- // ~/.local/bin/cicy-code symlink. checkLatest() reports {ok, latest,
53
- // installedVersion, network} without writing anything.
54
- sidecar: {
55
- install: () => relay("sidecar:install"),
56
- checkLatest: () => relay("sidecar:checkLatest"),
57
- },
49
+ // (sidecar install/checkLatest removed cicy-code is installed via
50
+ // `npx cicy-code` by the sidecar, no in-app downloader.)
58
51
  });
59
52
 
60
53
  console.log("[webview-preload] electronRPC + cicy.localTeams ready");
@@ -62,7 +62,18 @@ function registerTool(mcpServer, tools, title, description, schema, handler, opt
62
62
  tag,
63
63
  });
64
64
 
65
- mcpServer.tool(title, description, inputSchema, async (args) => {
65
+ // MCP SDK ≥1.29 rejects a plain JSON-Schema object as the params arg
66
+ // ("expected a Zod schema or ToolAnnotations, but received an unrecognized
67
+ // object"). It wants the raw Zod shape (an object of Zod types) and does its
68
+ // own JSON-Schema conversion. The hand-rolled `inputSchema` above is still
69
+ // used for our own `tools` catalog (express tool listing). Pass the real
70
+ // Zod shape here; falls back to {} (a valid empty raw shape) when absent.
71
+ const rawShape =
72
+ schema && schema._def && typeof schema._def.shape === "function"
73
+ ? schema._def.shape()
74
+ : (schema && schema.shape) || {};
75
+
76
+ mcpServer.tool(title, description, rawShape, async (args) => {
66
77
  try {
67
78
  const { executeTool } = require("./tool-executor");
68
79
  return await executeTool(title, args, {
@@ -1,17 +1,17 @@
1
1
  // Discover / probe / spawn the cicy-code daemon for the Electron app.
2
2
  //
3
- // Principle (2026-05-29): cicy-desktop does NOT bundle cicy-code. The
4
- // daemon is acquired three ways, in priority order:
5
- // 1. An already-running instance on :8008 (helper-installed, user-run,
6
- // or surviving from a previous launch). probeExisting wins reuse.
7
- // 2. <userData>/cicy-code/<platform>-<arch>/cicy-code written by
8
- // src/sidecar/installer.js when the user clicks the in-app installer
9
- // OR by the cloud Team Helper agent when it finishes onboarding.
10
- // 3. (no-op) if neither, return null — the homepage's Team Helper card
11
- // will guide the user through install. No "bundled" fallback exists.
3
+ // Principle (2026-06): the daemon is run via `npx cicy-code` — a single
4
+ // source of truth. cicy-desktop neither bundles nor downloads a binary; the
5
+ // per-version binary is fetched from npm by the launcher (CN: npmmirror).
6
+ // 1. An already-running instance on :8008 (user-run, npx, surviving from a
7
+ // previous launch). probeExisting wins reuse, never double-spawn.
8
+ // 2. Otherwise spawn `npx cicy-code` on mac/linux.
12
9
  //
13
- // Windows is not bundled either the daemon is WSL2-hosted via
14
- // src/sidecar/wsl.js. start() delegates there on win32.
10
+ // This replaced the old in-app installer (downloaded binary at
11
+ // ~/.local/bin/cicy-code), which raced the npx-launched daemon for :8008.
12
+ //
13
+ // Windows runs cicy-code in Docker (src/sidecar/docker.js); start() delegates
14
+ // there on win32. (The old WSL path was retired.)
15
15
 
16
16
  const fs = require("fs");
17
17
  const http = require("http");
@@ -20,32 +20,6 @@ const { spawn } = require("child_process");
20
20
 
21
21
  const DEFAULT_PORT = Number(process.env.CICY_CODE_PORT || 8008);
22
22
 
23
- function platformDir() {
24
- if (process.platform === "darwin") return "darwin";
25
- if (process.platform === "linux") return "linux";
26
- return null;
27
- }
28
- function archDir() {
29
- if (process.arch === "arm64") return "arm64";
30
- if (process.arch === "x64") return "x64";
31
- return null;
32
- }
33
-
34
- function bundledBinaryPath() {
35
- const plat = platformDir();
36
- const arch = archDir();
37
- if (!plat || !arch) return null;
38
- // Only the user-installed copy is considered. There is intentionally
39
- // no <App>/Contents/Resources/cicy-code fallback — cicy-desktop no
40
- // longer bundles the daemon (2026-05-29 principle).
41
- try {
42
- const installer = require("./installer");
43
- const userBin = installer.userBinary();
44
- if (userBin && fs.existsSync(userBin)) return userBin;
45
- } catch {}
46
- return null;
47
- }
48
-
49
23
  function probeExisting(port = DEFAULT_PORT, timeoutMs = 500) {
50
24
  return new Promise(resolve => {
51
25
  const req = http.get(
@@ -71,36 +45,25 @@ async function start({ logPath, port = DEFAULT_PORT, force = false } = {}) {
71
45
  }
72
46
 
73
47
  if (process.platform === "win32") {
74
- // Windows uses WSL2 to host the linux-amd64 binary. The wsl module owns
75
- // every wsl-touching command; here we just delegate.
48
+ // Windows runs cicy-code in Docker Desktop (the container's entrypoint
49
+ // npx-installs cicy-code). The docker module owns image-load-from-R2 +
50
+ // container run; here we just delegate. (Replaced the old WSL path.)
76
51
  try {
77
- const wsl = require("./wsl");
78
- const status = await wsl.checkStatus();
79
- if (!status.installed || !status.hasDistro) {
80
- console.warn(`[cicy-code-sidecar] WSL not ready (${JSON.stringify(status)}) — homepage will guide install`);
52
+ const docker = require("./docker");
53
+ const r = await docker.start({ port });
54
+ if (!r) {
55
+ console.warn("[cicy-code-sidecar] Docker not ready — homepage will guide install");
81
56
  return null;
82
57
  }
83
- if (!(await wsl.userInstalled())) {
84
- console.warn("[cicy-code-sidecar] cicy-code not installed in WSL yet homepage will trigger install");
85
- return null;
86
- }
87
- const r = await wsl.start({ port, force });
88
- // Treat WSL-internal pid as the child token so the outer code knows we're up.
89
- child = { wsl: true, pid: r.pid };
90
- console.log(`[cicy-code-sidecar] started inside WSL pid=${r.pid}`);
58
+ child = r; // { docker:true, container, id }
59
+ console.log(`[cicy-code-sidecar] started in Docker container ${r.container} (${r.id})`);
91
60
  return child;
92
61
  } catch (e) {
93
- console.warn(`[cicy-code-sidecar] WSL start failed: ${e.message}`);
62
+ console.warn(`[cicy-code-sidecar] Docker start failed: ${e.message}`);
94
63
  return null;
95
64
  }
96
65
  }
97
66
 
98
- const bin = bundledBinaryPath();
99
- if (!bin || !fs.existsSync(bin)) {
100
- console.warn(`[cicy-code-sidecar] no daemon binary found (user has not run the in-app installer or the cloud Team Helper); homepage's Team Helper card will guide install`);
101
- return null;
102
- }
103
-
104
67
  let stdio = ["ignore", "ignore", "ignore"];
105
68
  if (logPath) {
106
69
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
@@ -108,12 +71,25 @@ async function start({ logPath, port = DEFAULT_PORT, force = false } = {}) {
108
71
  stdio = ["ignore", fd, fd];
109
72
  }
110
73
 
111
- // cicy-code reads `PORT` env var. Strip the parent's PORT (set by the
112
- // worker process to its own listen port, e.g. 8101) so it doesn't leak
113
- // into the sidecar and clash with the worker's HTTP server.
114
- const env = { ...process.env, CICY_CODE_PORT: String(port), PORT: String(port) };
115
- child = spawn(bin, [], { stdio, detached: false, env });
116
- console.log(`[cicy-code-sidecar] spawned ${bin} pid=${child.pid} port=${port} log=${logPath || "(none)"}`);
74
+ // Run the daemon via `npx cicy-code` no bundled/downloaded binary. The
75
+ // launcher fetches the per-version binary from npm (default npmmirror for
76
+ // CN; override with CICY_NPM_REGISTRY) and does its own :8008 port hygiene.
77
+ // cicy-code reads PORT; we also set CICY_CODE_PORT and override the parent's
78
+ // PORT (the worker sets it to its own listen port, e.g. 8101) so it doesn't
79
+ // leak in and clash with the worker's HTTP server.
80
+ const registry = process.env.CICY_NPM_REGISTRY || "https://registry.npmmirror.com";
81
+ const env = {
82
+ ...process.env,
83
+ CICY_CODE_PORT: String(port),
84
+ PORT: String(port),
85
+ npm_config_registry: registry,
86
+ };
87
+ const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
88
+ const spec = process.env.CICY_CODE_VERSION
89
+ ? `cicy-code@${process.env.CICY_CODE_VERSION}`
90
+ : "cicy-code";
91
+ child = spawn(npxBin, ["-y", spec], { stdio, detached: false, env });
92
+ console.log(`[cicy-code-sidecar] spawned npx ${spec} pid=${child.pid} port=${port} registry=${registry} log=${logPath || "(none)"}`);
117
93
 
118
94
  child.on("exit", (code, signal) => {
119
95
  console.log(`[cicy-code-sidecar] exited code=${code} signal=${signal}`);
@@ -126,9 +102,9 @@ async function stop({ timeoutMs = 5000 } = {}) {
126
102
  if (!child) return;
127
103
  const p = child;
128
104
  child = null;
129
- // WSL-launched: not a real ChildProcess, kill via wsl pkill instead.
130
- if (p && p.wsl) {
131
- try { await require("./wsl").stop(); } catch {}
105
+ // Docker-launched (win32): not a real ChildProcess remove the container.
106
+ if (p && p.docker) {
107
+ try { await require("./docker").stop(); } catch {}
132
108
  return;
133
109
  }
134
110
  try { p.kill("SIGTERM"); } catch {}
@@ -141,4 +117,4 @@ async function stop({ timeoutMs = 5000 } = {}) {
141
117
  }
142
118
  }
143
119
 
144
- module.exports = { start, stop, probeExisting, bundledBinaryPath };
120
+ module.exports = { start, stop, probeExisting };
@@ -0,0 +1,113 @@
1
+ // Windows sidecar backend: run cicy-code inside a Docker container.
2
+ //
3
+ // Platform split (2026-06): mac/linux start cicy-code locally via `npx
4
+ // cicy-code` (see cicy-code.js); Windows runs it in Docker Desktop instead.
5
+ // The base-env image's entrypoint installs cicy-code from npm at container
6
+ // startup, so the image is version-independent. If the image isn't present
7
+ // locally it's loaded from R2 (CN-friendly, no Docker Hub pull):
8
+ // https://r2.deepfetch.de5.net/docker/cicy-code-latest.tar.gz
9
+ //
10
+ // The container maps :8008 and persists ~/cicy-ai in a named volume.
11
+ const { execFile } = require("child_process");
12
+ const https = require("https");
13
+ const http = require("http");
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const path = require("path");
17
+
18
+ const IMAGE = process.env.CICY_DOCKER_IMAGE || "cicybot/cicy-code:latest";
19
+ const R2_TARBALL = process.env.CICY_DOCKER_URL || "https://r2.deepfetch.de5.net/docker/cicy-code-latest.tar.gz";
20
+ const CONTAINER = process.env.CICY_DOCKER_CONTAINER || "cicy-code";
21
+ const VOLUME = process.env.CICY_DOCKER_VOLUME || "cicy-ai-data";
22
+ // CICY_* env vars forwarded into the container (team onboarding, version pin…).
23
+ const PASS_ENV = ["CICY_TEAM_TOKEN", "CICY_CODE_VERSION", "NPM_REGISTRY", "CICY_NPM_REGISTRY", "CICY_AGENTS", "ENABLE_CDN", "CICY_CLOUDFLARED_TOKEN"];
24
+
25
+ function run(args, { timeout = 30000 } = {}) {
26
+ return new Promise((resolve, reject) => {
27
+ execFile("docker", args, { timeout, windowsHide: true }, (err, stdout, stderr) => {
28
+ if (err) { err.stdout = String(stdout || ""); err.stderr = String(stderr || ""); return reject(err); }
29
+ resolve({ stdout: String(stdout), stderr: String(stderr) });
30
+ });
31
+ });
32
+ }
33
+
34
+ async function dockerOk() {
35
+ try { await run(["version", "--format", "{{.Server.Version}}"], { timeout: 8000 }); return true; }
36
+ catch { return false; }
37
+ }
38
+
39
+ async function imagePresent() {
40
+ try { await run(["image", "inspect", IMAGE], { timeout: 8000 }); return true; }
41
+ catch { return false; }
42
+ }
43
+
44
+ function download(url, dest, hops = 5) {
45
+ return new Promise((resolve, reject) => {
46
+ if (hops <= 0) return reject(new Error("too many redirects"));
47
+ const lib = url.startsWith("https:") ? https : http;
48
+ const req = lib.get(url, { timeout: 60000 }, (res) => {
49
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
50
+ res.resume();
51
+ return download(res.headers.location, dest, hops - 1).then(resolve, reject);
52
+ }
53
+ if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode}`)); }
54
+ const out = fs.createWriteStream(dest);
55
+ res.pipe(out);
56
+ out.on("finish", () => out.close(() => resolve(dest)));
57
+ out.on("error", reject);
58
+ });
59
+ req.on("error", reject);
60
+ req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
61
+ });
62
+ }
63
+
64
+ async function loadImage() {
65
+ const tmp = path.join(os.tmpdir(), `cicy-code-image-${process.pid}.tar.gz`);
66
+ console.log(`[docker-sidecar] downloading image from ${R2_TARBALL}`);
67
+ await download(R2_TARBALL, tmp);
68
+ console.log(`[docker-sidecar] docker load…`);
69
+ await run(["load", "-i", tmp], { timeout: 300000 });
70
+ try { fs.unlinkSync(tmp); } catch {}
71
+ }
72
+
73
+ async function checkStatus() {
74
+ const installed = await dockerOk();
75
+ return { installed, imagePresent: installed ? await imagePresent() : false };
76
+ }
77
+
78
+ // Start the container. Returns a sidecar child token { docker:true, container,
79
+ // id } or null when Docker isn't ready (homepage guides the user to install
80
+ // Docker Desktop).
81
+ async function start({ port = 8008 } = {}) {
82
+ if (!(await dockerOk())) {
83
+ console.warn("[docker-sidecar] Docker not available — homepage will guide install");
84
+ return null;
85
+ }
86
+ if (!(await imagePresent())) {
87
+ try { await loadImage(); }
88
+ catch (e) { console.warn(`[docker-sidecar] image load failed: ${e.message}`); return null; }
89
+ }
90
+ // Replace any stale container of the same name.
91
+ try { await run(["rm", "-f", CONTAINER]); } catch {}
92
+
93
+ const args = [
94
+ "run", "-d", "--name", CONTAINER, "--restart", "unless-stopped",
95
+ "-p", `${port}:8008`,
96
+ "-v", `${VOLUME}:/home/cicy/cicy-ai`,
97
+ ];
98
+ for (const k of PASS_ENV) {
99
+ if (process.env[k]) args.push("-e", `${k}=${process.env[k]}`);
100
+ }
101
+ args.push(IMAGE);
102
+
103
+ const { stdout } = await run(args, { timeout: 60000 });
104
+ const id = stdout.trim().slice(0, 12);
105
+ console.log(`[docker-sidecar] started container ${CONTAINER} (${id}) on :${port}`);
106
+ return { docker: true, container: CONTAINER, id };
107
+ }
108
+
109
+ async function stop() {
110
+ try { await run(["rm", "-f", CONTAINER]); } catch {}
111
+ }
112
+
113
+ module.exports = { start, stop, checkStatus, loadImage, imagePresent, dockerOk };
@@ -264,15 +264,9 @@ export default function App() {
264
264
  result = await window.cicy.localTeams.upgrade(msg.id);
265
265
  } else if (msg?.type === "localTeams:list") {
266
266
  result = { ok: true, teams: await window.cicy.localTeams.list({ refresh: true }) };
267
- } else if (msg?.type === "sidecar:install") {
268
- // Triggers the in-app installer: download latest cicy-code →
269
- // ~/.local/bin/cicy-code-<ver> + atomic-relink symlink. Note: does
270
- // NOT restart the running daemon. Pair with localTeams.upgrade for
271
- // a full restart cycle.
272
- result = await window.cicy.sidecar.install();
273
- } else if (msg?.type === "sidecar:checkLatest") {
274
- result = await window.cicy.sidecar.checkLatest();
275
267
  }
268
+ // (sidecar:install / sidecar:checkLatest removed — cicy-code is now
269
+ // installed via `npx cicy-code` by the sidecar, no in-app downloader.)
276
270
  // Force-refresh the team list so the new/removed/upgraded card
277
271
  // shows up before the next 30 s poll.
278
272
  fetchLocalTeams();