cicy-desktop 2.1.30 → 2.1.31

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.30",
3
+ "version": "2.1.31",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -1,17 +1,18 @@
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 src/sidecar/installer.js binary (~/.local/bin/
11
+ // cicy-code), which raced the npx-launched daemon for :8008. The in-app
12
+ // installer is no longer the daemon source.
13
+ //
14
+ // Windows is WSL2-hosted via src/sidecar/wsl.js; start() delegates there on
15
+ // win32 (npx-in-WSL migration tracked separately).
15
16
 
16
17
  const fs = require("fs");
17
18
  const http = require("http");
@@ -20,32 +21,6 @@ const { spawn } = require("child_process");
20
21
 
21
22
  const DEFAULT_PORT = Number(process.env.CICY_CODE_PORT || 8008);
22
23
 
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
24
  function probeExisting(port = DEFAULT_PORT, timeoutMs = 500) {
50
25
  return new Promise(resolve => {
51
26
  const req = http.get(
@@ -71,36 +46,25 @@ async function start({ logPath, port = DEFAULT_PORT, force = false } = {}) {
71
46
  }
72
47
 
73
48
  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.
49
+ // Windows runs cicy-code in Docker Desktop (the container's entrypoint
50
+ // npx-installs cicy-code). The docker module owns image-load-from-R2 +
51
+ // container run; here we just delegate. (Replaced the old WSL path.)
76
52
  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`);
53
+ const docker = require("./docker");
54
+ const r = await docker.start({ port });
55
+ if (!r) {
56
+ console.warn("[cicy-code-sidecar] Docker not ready — homepage will guide install");
81
57
  return null;
82
58
  }
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}`);
59
+ child = r; // { docker:true, container, id }
60
+ console.log(`[cicy-code-sidecar] started in Docker container ${r.container} (${r.id})`);
91
61
  return child;
92
62
  } catch (e) {
93
- console.warn(`[cicy-code-sidecar] WSL start failed: ${e.message}`);
63
+ console.warn(`[cicy-code-sidecar] Docker start failed: ${e.message}`);
94
64
  return null;
95
65
  }
96
66
  }
97
67
 
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
68
  let stdio = ["ignore", "ignore", "ignore"];
105
69
  if (logPath) {
106
70
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
@@ -108,12 +72,25 @@ async function start({ logPath, port = DEFAULT_PORT, force = false } = {}) {
108
72
  stdio = ["ignore", fd, fd];
109
73
  }
110
74
 
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)"}`);
75
+ // Run the daemon via `npx cicy-code` no bundled/downloaded binary. The
76
+ // launcher fetches the per-version binary from npm (default npmmirror for
77
+ // CN; override with CICY_NPM_REGISTRY) and does its own :8008 port hygiene.
78
+ // cicy-code reads PORT; we also set CICY_CODE_PORT and override the parent's
79
+ // PORT (the worker sets it to its own listen port, e.g. 8101) so it doesn't
80
+ // leak in and clash with the worker's HTTP server.
81
+ const registry = process.env.CICY_NPM_REGISTRY || "https://registry.npmmirror.com";
82
+ const env = {
83
+ ...process.env,
84
+ CICY_CODE_PORT: String(port),
85
+ PORT: String(port),
86
+ npm_config_registry: registry,
87
+ };
88
+ const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
89
+ const spec = process.env.CICY_CODE_VERSION
90
+ ? `cicy-code@${process.env.CICY_CODE_VERSION}`
91
+ : "cicy-code";
92
+ child = spawn(npxBin, ["-y", spec], { stdio, detached: false, env });
93
+ console.log(`[cicy-code-sidecar] spawned npx ${spec} pid=${child.pid} port=${port} registry=${registry} log=${logPath || "(none)"}`);
117
94
 
118
95
  child.on("exit", (code, signal) => {
119
96
  console.log(`[cicy-code-sidecar] exited code=${code} signal=${signal}`);
@@ -126,7 +103,12 @@ async function stop({ timeoutMs = 5000 } = {}) {
126
103
  if (!child) return;
127
104
  const p = child;
128
105
  child = null;
129
- // WSL-launched: not a real ChildProcess, kill via wsl pkill instead.
106
+ // Docker-launched (win32): not a real ChildProcess remove the container.
107
+ if (p && p.docker) {
108
+ try { await require("./docker").stop(); } catch {}
109
+ return;
110
+ }
111
+ // WSL-launched (legacy): not a real ChildProcess, kill via wsl pkill instead.
130
112
  if (p && p.wsl) {
131
113
  try { await require("./wsl").stop(); } catch {}
132
114
  return;
@@ -141,4 +123,4 @@ async function stop({ timeoutMs = 5000 } = {}) {
141
123
  }
142
124
  }
143
125
 
144
- module.exports = { start, stop, probeExisting, bundledBinaryPath };
126
+ 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 };