cicy-desktop 2.1.331 → 2.1.333
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
|
@@ -4,11 +4,23 @@
|
|
|
4
4
|
// Homepage window — primary CiCy Desktop window. Singleton; closing it
|
|
5
5
|
// does NOT quit the app.
|
|
6
6
|
//
|
|
7
|
-
// URL selection
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// URL selection: the homepage IS the deployed web build
|
|
8
|
+
// (https://desktop.cicy-ai.com — the desktop-render Worker). Shipping homepage
|
|
9
|
+
// changes then means deploying the Worker, not shipping a new desktop build.
|
|
10
|
+
//
|
|
11
|
+
// The bundled file:// snapshot stays in the package as the FALLBACK: a
|
|
12
|
+
// main-frame load failure, or a load that never commits within 15s (a blank 200
|
|
13
|
+
// from a broken deploy), swaps to it — a CDN outage can never leave the app with
|
|
14
|
+
// no homepage. Set CICY_HOMEPAGE_URL to point somewhere else (e.g. a vite dev
|
|
15
|
+
// server), or prefs.homepageRemote:false to pin a machine to the snapshot.
|
|
16
|
+
//
|
|
17
|
+
// Note what this grants: the home tab runs homepage-preload, whose bridge is the
|
|
18
|
+
// UNGUARDED "rpc" channel — no origin gate, no dangerous-tool gate (see
|
|
19
|
+
// utils/rpc-guard), because it predates the homepage ever being remote. The
|
|
20
|
+
// homepage origin therefore has ungated exec_*/file_* on the machine, so
|
|
21
|
+
// desktop.cicy-ai.com must be treated as first-party code, exactly like the
|
|
22
|
+
// bundle it replaces: whoever can deploy that Worker can run commands on every
|
|
23
|
+
// desktop.
|
|
12
24
|
|
|
13
25
|
const path = require("path");
|
|
14
26
|
const { BrowserWindow } = require("electron");
|
|
@@ -19,9 +31,56 @@ const FIXED_WIDTH = 930;
|
|
|
19
31
|
const FIXED_HEIGHT = 640;
|
|
20
32
|
|
|
21
33
|
const LOCAL_INDEX = path.join(__dirname, "homepage-react", "index.html");
|
|
34
|
+
const LOCAL_URL = `file://${LOCAL_INDEX}`;
|
|
35
|
+
const REMOTE_HOMEPAGE = "https://desktop.cicy-ai.com/";
|
|
36
|
+
const REMOTE_FALLBACK_MS = 15000;
|
|
37
|
+
|
|
38
|
+
function readHomepagePrefs() {
|
|
39
|
+
try {
|
|
40
|
+
const os = require("os");
|
|
41
|
+
const p = path.join(os.homedir(), "cicy-ai", "db", "prefs.json");
|
|
42
|
+
return JSON.parse(require("fs").readFileSync(p, "utf8")) || {};
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
function pickHomepageURL() {
|
|
24
|
-
|
|
49
|
+
const env = String(process.env.CICY_HOMEPAGE_URL || "").trim();
|
|
50
|
+
if (env) return env;
|
|
51
|
+
// Opt OUT only: an explicit false pins this machine to the bundled snapshot.
|
|
52
|
+
try {
|
|
53
|
+
if (readHomepagePrefs().homepageRemote === false) return LOCAL_URL;
|
|
54
|
+
} catch {}
|
|
55
|
+
return REMOTE_HOMEPAGE;
|
|
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,119 @@
|
|
|
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 deployed web build", () => {
|
|
58
|
+
withHome(() => {
|
|
59
|
+
const { pickHomepageURL, REMOTE_HOMEPAGE } = loadPicker();
|
|
60
|
+
assert.equal(pickHomepageURL(), REMOTE_HOMEPAGE);
|
|
61
|
+
assert.equal(REMOTE_HOMEPAGE, "https://desktop.cicy-ai.com/");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("prefs.homepageRemote:false pins a machine to the bundled snapshot", () => {
|
|
66
|
+
withHome((home) => {
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(home, "cicy-ai", "db", "prefs.json"),
|
|
69
|
+
JSON.stringify({ homepageRemote: false })
|
|
70
|
+
);
|
|
71
|
+
const { pickHomepageURL, LOCAL_URL } = loadPicker();
|
|
72
|
+
assert.equal(pickHomepageURL(), LOCAL_URL);
|
|
73
|
+
assert.match(pickHomepageURL(), /^file:\/\//);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("any value other than an explicit false stays on the web build", () => {
|
|
78
|
+
withHome((home) => {
|
|
79
|
+
for (const v of [true, "false", 0, null]) {
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
path.join(home, "cicy-ai", "db", "prefs.json"),
|
|
82
|
+
JSON.stringify({ homepageRemote: v })
|
|
83
|
+
);
|
|
84
|
+
const { pickHomepageURL, REMOTE_HOMEPAGE } = loadPicker();
|
|
85
|
+
assert.equal(pickHomepageURL(), REMOTE_HOMEPAGE, `homepageRemote=${JSON.stringify(v)}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("CICY_HOMEPAGE_URL wins over prefs and the default", () => {
|
|
91
|
+
withHome((home) => {
|
|
92
|
+
fs.writeFileSync(
|
|
93
|
+
path.join(home, "cicy-ai", "db", "prefs.json"),
|
|
94
|
+
JSON.stringify({ homepageRemote: false })
|
|
95
|
+
);
|
|
96
|
+
process.env.CICY_HOMEPAGE_URL = "http://127.0.0.1:5173/";
|
|
97
|
+
try {
|
|
98
|
+
assert.equal(loadPicker().pickHomepageURL(), "http://127.0.0.1:5173/");
|
|
99
|
+
} finally {
|
|
100
|
+
delete process.env.CICY_HOMEPAGE_URL;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("a malformed prefs file keeps the default rather than throwing", () => {
|
|
106
|
+
withHome((home) => {
|
|
107
|
+
fs.writeFileSync(path.join(home, "cicy-ai", "db", "prefs.json"), "{ not json");
|
|
108
|
+
const { pickHomepageURL, REMOTE_HOMEPAGE } = loadPicker();
|
|
109
|
+
assert.equal(pickHomepageURL(), REMOTE_HOMEPAGE);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a remote homepage always has a way back to the bundled snapshot", () => {
|
|
114
|
+
assert.match(src, /wc\.on\("did-fail-load"/);
|
|
115
|
+
assert.match(src, /setTimeout\(\(\) => toLocal\(`no load within/); // blank page that never commits
|
|
116
|
+
assert.match(src, /if \(url\.startsWith\("file:\/\/"\)\) \{ settled = true; return; \}/); // never loops on local
|
|
117
|
+
assert.match(src, /wireRemoteFallback\(wc\)/); // resident home tab
|
|
118
|
+
assert.match(src, /wireRemoteFallback\(homepage\.webContents\)/); // standalone window
|
|
119
|
+
});
|