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.
- package/bin/cicy-desktop +84 -6
- package/package.json +1 -1
- package/src/backends/homepage-preload.js +2 -10
- package/src/backends/homepage-react/assets/index-CzuQdFw8.js +49 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/ipc.js +2 -3
- package/src/backends/sidecar-ipc.js +18 -158
- package/src/backends/webview-preload.js +2 -9
- package/src/server/tool-registry.js +12 -1
- package/src/sidecar/cicy-code.js +44 -68
- package/src/sidecar/docker.js +113 -0
- package/workers/render/src/App.jsx +2 -8
- package/src/backends/homepage-react/assets/index-B8FrtpTX.js +0 -49
- package/src/sidecar/installer.js +0 -672
- package/src/sidecar/wsl.js +0 -585
|
@@ -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-
|
|
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>
|
package/src/backends/ipc.js
CHANGED
|
@@ -129,9 +129,8 @@ function register(opts = {}) {
|
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// sidecar:
|
|
8
|
-
// sidecar:
|
|
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
|
-
//
|
|
11
|
-
//
|
|
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
|
|
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
|
|
34
|
-
|
|
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
|
|
58
|
-
//
|
|
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
|
-
|
|
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
|
|
66
|
-
//
|
|
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
|
|
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
|
-
//
|
|
50
|
-
//
|
|
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
|
-
|
|
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, {
|
package/src/sidecar/cicy-code.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Discover / probe / spawn the cicy-code daemon for the Electron app.
|
|
2
2
|
//
|
|
3
|
-
// Principle (2026-
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
//
|
|
14
|
-
//
|
|
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
|
|
75
|
-
//
|
|
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
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
80
|
-
console.warn(
|
|
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
|
-
|
|
84
|
-
|
|
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]
|
|
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
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
//
|
|
130
|
-
if (p && p.
|
|
131
|
-
try { await require("./
|
|
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
|
|
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();
|