cicy-desktop 2.1.331 → 2.1.332

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.331",
3
+ "version": "2.1.332",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -4,11 +4,24 @@
4
4
  // Homepage window — primary CiCy Desktop window. Singleton; closing it
5
5
  // does NOT quit the app.
6
6
  //
7
- // URL selection (HARDCODED no env/dev-URL switch, no Vite, no remote): ALL
8
- // platforms load the bundled local file:// SPA works offline, fast, no
9
- // remote dependency, no mixed-content concerns when embedding the
10
- // team-assistant webview (the cicy-desktop preload's IPC bridge still
11
- // attaches).
7
+ // URL selection: the bundled local file:// SPA by DEFAULT works offline, fast,
8
+ // no remote dependency, no mixed-content concerns when embedding the
9
+ // team-assistant webview (the cicy-desktop preload's IPC bridge still attaches).
10
+ //
11
+ // The same SPA is also deployed as the desktop-render Worker on
12
+ // https://desktop.cicy-ai.com, which is what makes "ship UI without shipping the
13
+ // app" possible. Pointing the homepage at it is OPT-IN, and deliberately not the
14
+ // default: the home tab runs homepage-preload, whose bridge is the UNGUARDED
15
+ // "rpc" channel (no origin gate, no dangerous-tool gate — see utils/rpc-guard),
16
+ // because it was only ever loaded from file://. A remote origin on that channel
17
+ // gets ungated exec_*/file_* on every desktop, so whoever controls the domain
18
+ // controls the fleet. Turn it on per machine with CICY_HOMEPAGE_URL (any URL) or
19
+ // prefs.homepageRemote:true (uses REMOTE_HOMEPAGE), and only for an origin you
20
+ // trust exactly as much as the bundled bundle itself.
21
+ //
22
+ // Either way the remote load is never a one-way door: a main-frame failure (or a
23
+ // load that never commits) falls back to the bundled snapshot, so a CDN outage
24
+ // cannot leave the app with no homepage.
12
25
 
13
26
  const path = require("path");
14
27
  const { BrowserWindow } = require("electron");
@@ -19,9 +32,55 @@ const FIXED_WIDTH = 930;
19
32
  const FIXED_HEIGHT = 640;
20
33
 
21
34
  const LOCAL_INDEX = path.join(__dirname, "homepage-react", "index.html");
35
+ const LOCAL_URL = `file://${LOCAL_INDEX}`;
36
+ const REMOTE_HOMEPAGE = "https://desktop.cicy-ai.com/";
37
+ const REMOTE_FALLBACK_MS = 15000;
38
+
39
+ function readHomepagePrefs() {
40
+ try {
41
+ const os = require("os");
42
+ const p = path.join(os.homedir(), "cicy-ai", "db", "prefs.json");
43
+ return JSON.parse(require("fs").readFileSync(p, "utf8")) || {};
44
+ } catch {
45
+ return {};
46
+ }
47
+ }
22
48
 
23
49
  function pickHomepageURL() {
24
- return `file://${LOCAL_INDEX}`;
50
+ const env = String(process.env.CICY_HOMEPAGE_URL || "").trim();
51
+ if (env) return env;
52
+ try {
53
+ if (readHomepagePrefs().homepageRemote === true) return REMOTE_HOMEPAGE;
54
+ } catch {}
55
+ return LOCAL_URL;
56
+ }
57
+
58
+ // Fall back to the bundled snapshot when a remote homepage fails to load or
59
+ // never commits. Idempotent per webContents; a no-op once we are already local.
60
+ function wireRemoteFallback(wc) {
61
+ if (!wc || wc.isDestroyed() || wc.__cicyHomeFallbackWired) return;
62
+ let current = "";
63
+ try { current = wc.getURL(); } catch {}
64
+ if (!current || current.startsWith("file://")) {
65
+ // getURL() is empty until the first commit, so only skip a known-local load.
66
+ if (current) return;
67
+ }
68
+ wc.__cicyHomeFallbackWired = true;
69
+ let settled = false;
70
+ const toLocal = (reason) => {
71
+ if (settled || wc.isDestroyed()) return;
72
+ let url = "";
73
+ try { url = wc.getURL(); } catch {}
74
+ if (url.startsWith("file://")) { settled = true; return; }
75
+ settled = true;
76
+ log.warn(`[homepage] remote homepage unusable (${reason}) → bundled snapshot`);
77
+ try { wc.loadURL(LOCAL_URL); } catch (e) { log.error(`[homepage] fallback failed: ${e.message}`); }
78
+ };
79
+ wc.on("did-fail-load", (_e, code, desc, _url, isMainFrame) => {
80
+ if (isMainFrame) toLocal(`${code} ${desc}`);
81
+ });
82
+ wc.once("did-finish-load", () => { settled = true; });
83
+ setTimeout(() => toLocal(`no load within ${REMOTE_FALLBACK_MS}ms`), REMOTE_FALLBACK_MS);
25
84
  }
26
85
 
27
86
  let homepage = null; // standalone fallback window (only if the tab engine fails)
@@ -40,6 +99,7 @@ async function openHomepage(opts = {}) {
40
99
  if (wc) {
41
100
  homeTabWc = wc;
42
101
  try { wc.once("destroyed", () => { if (homeTabWc === wc) homeTabWc = null; }); } catch {}
102
+ try { wireRemoteFallback(wc); } catch (e) { log.warn(`[homepage] fallback wiring: ${e.message}`); }
43
103
  }
44
104
  log.info(`[homepage] opened as profile-0 resident tab (wc=${wc && wc.id})`);
45
105
  return win;
@@ -103,6 +163,7 @@ async function openHomepageStandalone() {
103
163
 
104
164
  const target = pickHomepageURL();
105
165
  log.info(`[homepage] loading ${target}`);
166
+ try { wireRemoteFallback(homepage.webContents); } catch (e) { log.warn(`[homepage] fallback wiring: ${e.message}`); }
106
167
  homepage.loadURL(target);
107
168
 
108
169
  // Pipe renderer console + load failures to main-process stdout so we can
@@ -0,0 +1,106 @@
1
+ // Copyright 2026 CiCy AI
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The homepage URL was hardcoded to file://, so deploying the same SPA as the
5
+ // desktop-render Worker (desktop.cicy-ai.com) changed nothing on any desktop —
6
+ // the app never fetched it. It can now be pointed at the web build, but only
7
+ // deliberately: the home tab runs homepage-preload, whose bridge is the
8
+ // UNGUARDED "rpc" channel, so a remote origin there gets ungated exec_*/file_*.
9
+ // Hence opt-in + an automatic fall back to the bundled snapshot.
10
+ const test = require("node:test");
11
+ const assert = require("node:assert/strict");
12
+ const fs = require("node:fs");
13
+ const os = require("node:os");
14
+ const path = require("node:path");
15
+
16
+ const SRC = path.join(__dirname, "..", "src", "backends", "homepage-window.js");
17
+ const src = fs.readFileSync(SRC, "utf8");
18
+
19
+ function withHome(fn) {
20
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "cicy-home-"));
21
+ const prevHome = process.env.HOME,
22
+ prevUp = process.env.USERPROFILE;
23
+ const prevUrl = process.env.CICY_HOMEPAGE_URL;
24
+ process.env.HOME = home;
25
+ process.env.USERPROFILE = home;
26
+ delete process.env.CICY_HOMEPAGE_URL;
27
+ fs.mkdirSync(path.join(home, "cicy-ai", "db"), { recursive: true });
28
+ try {
29
+ return fn(home);
30
+ } finally {
31
+ process.env.HOME = prevHome;
32
+ if (prevUp === undefined) delete process.env.USERPROFILE;
33
+ else process.env.USERPROFILE = prevUp;
34
+ if (prevUrl === undefined) delete process.env.CICY_HOMEPAGE_URL;
35
+ else process.env.CICY_HOMEPAGE_URL = prevUrl;
36
+ }
37
+ }
38
+
39
+ // homepage-window requires electron; exercise the real pickHomepageURL by
40
+ // evaluating just that function plus the helper it depends on.
41
+ function loadPicker() {
42
+ const start = src.indexOf("const LOCAL_INDEX");
43
+ const end = src.indexOf("// Fall back to the bundled snapshot");
44
+ const body = src.slice(start, end);
45
+ const mod = { exports: {} };
46
+ const fn = new Function(
47
+ "require",
48
+ "path",
49
+ "module",
50
+ "__dirname",
51
+ body + "\nmodule.exports = { pickHomepageURL, LOCAL_URL, REMOTE_HOMEPAGE };"
52
+ );
53
+ fn(require, path, mod, path.dirname(SRC));
54
+ return mod.exports;
55
+ }
56
+
57
+ test("defaults to the bundled file:// snapshot", () => {
58
+ withHome(() => {
59
+ const { pickHomepageURL, LOCAL_URL } = loadPicker();
60
+ assert.equal(pickHomepageURL(), LOCAL_URL);
61
+ assert.match(pickHomepageURL(), /^file:\/\//);
62
+ });
63
+ });
64
+
65
+ test("prefs.homepageRemote opts in to the deployed Worker", () => {
66
+ withHome((home) => {
67
+ fs.writeFileSync(
68
+ path.join(home, "cicy-ai", "db", "prefs.json"),
69
+ JSON.stringify({ homepageRemote: true })
70
+ );
71
+ const { pickHomepageURL, REMOTE_HOMEPAGE } = loadPicker();
72
+ assert.equal(pickHomepageURL(), REMOTE_HOMEPAGE);
73
+ assert.equal(REMOTE_HOMEPAGE, "https://desktop.cicy-ai.com/");
74
+ });
75
+ });
76
+
77
+ test("CICY_HOMEPAGE_URL wins over prefs and the default", () => {
78
+ withHome((home) => {
79
+ fs.writeFileSync(
80
+ path.join(home, "cicy-ai", "db", "prefs.json"),
81
+ JSON.stringify({ homepageRemote: true })
82
+ );
83
+ process.env.CICY_HOMEPAGE_URL = "http://127.0.0.1:5173/";
84
+ try {
85
+ assert.equal(loadPicker().pickHomepageURL(), "http://127.0.0.1:5173/");
86
+ } finally {
87
+ delete process.env.CICY_HOMEPAGE_URL;
88
+ }
89
+ });
90
+ });
91
+
92
+ test("a malformed prefs file falls back to local rather than throwing", () => {
93
+ withHome((home) => {
94
+ fs.writeFileSync(path.join(home, "cicy-ai", "db", "prefs.json"), "{ not json");
95
+ const { pickHomepageURL, LOCAL_URL } = loadPicker();
96
+ assert.equal(pickHomepageURL(), LOCAL_URL);
97
+ });
98
+ });
99
+
100
+ test("a remote homepage always has a way back to the bundled snapshot", () => {
101
+ assert.match(src, /wc\.on\("did-fail-load"/);
102
+ assert.match(src, /setTimeout\(\(\) => toLocal\(`no load within/); // blank page that never commits
103
+ assert.match(src, /if \(url\.startsWith\("file:\/\/"\)\) \{ settled = true; return; \}/); // never loops on local
104
+ assert.match(src, /wireRemoteFallback\(wc\)/); // resident home tab
105
+ assert.match(src, /wireRemoteFallback\(homepage\.webContents\)/); // standalone window
106
+ });